From 4e85a57612768d407fa6ae8925c64d4a4bf199c4 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Wed, 23 Feb 2022 03:45:41 +0800 Subject: [PATCH] Fix enumerator not disposed when arbitrary projection is enumerated (#132) * Add failing test for should dispose enumerator * Dispose enumerator when arbitrary projection is enumerated Let's `foreach` the enumerable, which automatically disposes enumerator. * Fix typos --- .../Impl/SourceInjectedQueryProvider.cs | 9 +- .../Impl/SourceInjectedQuery.cs | 108 ++++++++++++++++++ 2 files changed, 112 insertions(+), 5 deletions(-) diff --git a/src/AutoMapper.Extensions.ExpressionMapping/Impl/SourceInjectedQueryProvider.cs b/src/AutoMapper.Extensions.ExpressionMapping/Impl/SourceInjectedQueryProvider.cs index e0c855c..2a3d040 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/Impl/SourceInjectedQueryProvider.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/Impl/SourceInjectedQueryProvider.cs @@ -111,21 +111,20 @@ public TResult Execute(Expression expression) destResult = (IQueryable)GetMapExpressions(queryExpressions).Aggregate(sourceResult, Select); } // case #2: query is arbitrary ("manual") projection - // exaple: users.UseAsDataSource().For().Select(user => user.Age).ToList() + // example: users.UseAsDataSource().For().Select(user => user.Age).ToList() // in case an arbitrary select-statement is enumerated, we do not need to map the expression at all - // and cann safely return it + // and can safely return it else if (IsProjection(resultType, sourceExpression)) { var sourceResult = _dataSource.Provider.CreateQuery(sourceExpression); - var enumerator = sourceResult.GetEnumerator(); var elementType = ElementTypeHelper.GetElementType(typeof(TResult)); var constructorInfo = typeof(List<>).MakeGenericType(elementType).GetDeclaredConstructor(new Type[0]); if (constructorInfo != null) { var listInstance = (IList)constructorInfo.Invoke(null); - while (enumerator.MoveNext()) + foreach (var element in sourceResult) { - listInstance.Add(enumerator.Current); + listInstance.Add(element); } destResult = listInstance; } diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/Impl/SourceInjectedQuery.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/Impl/SourceInjectedQuery.cs index 604344f..01bb42b 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/Impl/SourceInjectedQuery.cs +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/Impl/SourceInjectedQuery.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -622,6 +623,23 @@ public void Shoud_convert_type_changes() // //result.Any(s => s.DestValue > 6).ShouldBeTrue(); //} + [Fact] + public void Should_dispose_enumerator_when_arbitrary_projection_is_enumerated() + { + // Arrange + var mapper = SetupAutoMapper(); + var source = new NotSingleQueryingEnumerable(); + + // Act + source.AsQueryable() + .UseAsDataSource(mapper).For() + .Select(m => m.Name) + .ToList(); + + // Assert + NotRelationalDataReader.Instance.ShouldBeNull(); + } + private static IMapper SetupAutoMapper() { var config = new MapperConfiguration(cfg => @@ -825,6 +843,96 @@ public class ResourceDto public string Title { get; set; } public bool HasEditPermission { get; set; } } + + public class NotRelationalDataReader : IDisposable + { + public static NotRelationalDataReader Instance { get; set; } + + public NotRelationalDataReader() + { + Instance = this; + } + + public void Dispose() + { + Instance = null; + } + } + + /// + /// Based on Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable<T> + /// for and . + /// + public class NotSingleQueryingEnumerable : IQueryable, IQueryProvider + { + public Type ElementType => throw new NotImplementedException(); + + public Expression Expression => Expression.Constant(this); + + public IQueryProvider Provider => this; + + public IQueryable CreateQuery(Expression expression) + { + return this; + } + + public IQueryable CreateQuery(Expression expression) + { + throw new NotImplementedException(); + } + + public object Execute(Expression expression) + { + throw new NotImplementedException(); + } + + public TResult Execute(Expression expression) + { + throw new NotImplementedException(); + } + + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(); + } + + private sealed class Enumerator : IEnumerator + { + private NotRelationalDataReader _dataReader; + + public T Current => throw new NotImplementedException(); + + object IEnumerator.Current => throw new NotImplementedException(); + + public void Dispose() + { + if (_dataReader is not null) + { + _dataReader.Dispose(); + _dataReader = null; + } + } + + public bool MoveNext() + { + if (_dataReader == null) + { + _dataReader = new NotRelationalDataReader(); + } + return false; + } + + public void Reset() + { + throw new NotImplementedException(); + } + } + } }