Skip to content

Commit 4e85a57

Browse files
authored
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
1 parent e16bb6f commit 4e85a57

File tree

2 files changed

+112
-5
lines changed

2 files changed

+112
-5
lines changed

src/AutoMapper.Extensions.ExpressionMapping/Impl/SourceInjectedQueryProvider.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,21 +111,20 @@ public TResult Execute<TResult>(Expression expression)
111111
destResult = (IQueryable<TDestination>)GetMapExpressions(queryExpressions).Aggregate(sourceResult, Select);
112112
}
113113
// case #2: query is arbitrary ("manual") projection
114-
// exaple: users.UseAsDataSource().For<UserDto>().Select(user => user.Age).ToList()
114+
// example: users.UseAsDataSource().For<UserDto>().Select(user => user.Age).ToList()
115115
// in case an arbitrary select-statement is enumerated, we do not need to map the expression at all
116-
// and cann safely return it
116+
// and can safely return it
117117
else if (IsProjection(resultType, sourceExpression))
118118
{
119119
var sourceResult = _dataSource.Provider.CreateQuery(sourceExpression);
120-
var enumerator = sourceResult.GetEnumerator();
121120
var elementType = ElementTypeHelper.GetElementType(typeof(TResult));
122121
var constructorInfo = typeof(List<>).MakeGenericType(elementType).GetDeclaredConstructor(new Type[0]);
123122
if (constructorInfo != null)
124123
{
125124
var listInstance = (IList)constructorInfo.Invoke(null);
126-
while (enumerator.MoveNext())
125+
foreach (var element in sourceResult)
127126
{
128-
listInstance.Add(enumerator.Current);
127+
listInstance.Add(element);
129128
}
130129
destResult = listInstance;
131130
}

tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/Impl/SourceInjectedQuery.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.ComponentModel;
45
using System.Linq;
@@ -622,6 +623,23 @@ public void Shoud_convert_type_changes()
622623
// //result.Any(s => s.DestValue > 6).ShouldBeTrue();
623624
//}
624625

626+
[Fact]
627+
public void Should_dispose_enumerator_when_arbitrary_projection_is_enumerated()
628+
{
629+
// Arrange
630+
var mapper = SetupAutoMapper();
631+
var source = new NotSingleQueryingEnumerable<Detail>();
632+
633+
// Act
634+
source.AsQueryable()
635+
.UseAsDataSource(mapper).For<DetailDto>()
636+
.Select(m => m.Name)
637+
.ToList();
638+
639+
// Assert
640+
NotRelationalDataReader.Instance.ShouldBeNull();
641+
}
642+
625643
private static IMapper SetupAutoMapper()
626644
{
627645
var config = new MapperConfiguration(cfg =>
@@ -825,6 +843,96 @@ public class ResourceDto
825843
public string Title { get; set; }
826844
public bool HasEditPermission { get; set; }
827845
}
846+
847+
public class NotRelationalDataReader : IDisposable
848+
{
849+
public static NotRelationalDataReader Instance { get; set; }
850+
851+
public NotRelationalDataReader()
852+
{
853+
Instance = this;
854+
}
855+
856+
public void Dispose()
857+
{
858+
Instance = null;
859+
}
860+
}
861+
862+
/// <remarks>
863+
/// Based on <see href="https://github.com/dotnet/efcore/blob/08e5ebd/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs">Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable&lt;T&gt;</see>
864+
/// for <see cref="Enumerator.MoveNext"/> and <see cref="Enumerator.Dispose"/>.
865+
/// </remarks>
866+
public class NotSingleQueryingEnumerable<T> : IQueryable<T>, IQueryProvider
867+
{
868+
public Type ElementType => throw new NotImplementedException();
869+
870+
public Expression Expression => Expression.Constant(this);
871+
872+
public IQueryProvider Provider => this;
873+
874+
public IQueryable CreateQuery(Expression expression)
875+
{
876+
return this;
877+
}
878+
879+
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
880+
{
881+
throw new NotImplementedException();
882+
}
883+
884+
public object Execute(Expression expression)
885+
{
886+
throw new NotImplementedException();
887+
}
888+
889+
public TResult Execute<TResult>(Expression expression)
890+
{
891+
throw new NotImplementedException();
892+
}
893+
894+
public IEnumerator<T> GetEnumerator()
895+
{
896+
throw new NotImplementedException();
897+
}
898+
899+
IEnumerator IEnumerable.GetEnumerator()
900+
{
901+
return new Enumerator();
902+
}
903+
904+
private sealed class Enumerator : IEnumerator<T>
905+
{
906+
private NotRelationalDataReader _dataReader;
907+
908+
public T Current => throw new NotImplementedException();
909+
910+
object IEnumerator.Current => throw new NotImplementedException();
911+
912+
public void Dispose()
913+
{
914+
if (_dataReader is not null)
915+
{
916+
_dataReader.Dispose();
917+
_dataReader = null;
918+
}
919+
}
920+
921+
public bool MoveNext()
922+
{
923+
if (_dataReader == null)
924+
{
925+
_dataReader = new NotRelationalDataReader();
926+
}
927+
return false;
928+
}
929+
930+
public void Reset()
931+
{
932+
throw new NotImplementedException();
933+
}
934+
}
935+
}
828936
}
829937

830938

0 commit comments

Comments
 (0)