Skip to content

Commit

Permalink
3.1.0 release
Browse files Browse the repository at this point in the history
  • Loading branch information
Xriuk committed Apr 26, 2024
1 parent e4d7b06 commit c623754
Show file tree
Hide file tree
Showing 23 changed files with 291 additions and 154 deletions.
4 changes: 4 additions & 0 deletions docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ favicon_ico: "/assets/images/icon.png"

search_enabled: true

nav_external_links:
- title: GitHub
url: https://github.com/Xriuk/NeatMapper

back_to_top: true
back_to_top_text: "Back to top"

Expand Down
5 changes: 3 additions & 2 deletions docs/advanced-options/collection-mapping-and-projection.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public class MyMaps :
# Match elements in collections

{: .highlight }
The below **does not apply** to projectors.
The section below **does not apply** to projectors.

When merging to an existing collection, by default all the object present are removed and new ones are mapped and added (by using `INewMap<TSource, TDestination>` or `IMergeMap<TSource, TDestination>` in this order).

Expand Down Expand Up @@ -114,7 +114,8 @@ You can also match whole hierarchies by creating a `IHierarchyMatchMap<TSource,

# Destination collection cleanup

The below **does not apply** to projectors.
{: .highlight }
The section below **does not apply** to projectors.

Any element in the destination collection which do not have a corresponding element
in the source collection is removed by default, you can disable this
Expand Down
8 changes: 7 additions & 1 deletion src/NeatMapper.EntityFrameworkCore/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
# Changelog

## [3.1.0] - Unreleased
## [3.1.0] - 2024-04-26

### Added

- .NET 8.0 support

### Changed

- Updated NeatMapper dependency version
- `EntityFrameworkCoreMatcher` now handles shadow keys too, and now it requires a `DbContext` type and optionally a `ServiceProvider` in its constructor
- Instead of `TaskCanceledException` which where caught and re-thrown directly by maps and mappers (instead of being wrapped in `MappingException` like the others) now `OperationCanceledException`s are caught and re-thrown, this is backwards compatible, since `TaskCanceledException` is derived from it, but now other exceptions can be caught too
- `EntityFrameworkCoreProjector` no longer throws `MapNotFoundException` in case of a disposed `DbContext`, instead the exception is now wrapped in a `ProjectionException`

### Fixed

- `AsyncEntityFrameworkCoreMapper` now correctly resolves the `DbContext` from an overridden `IServiceProvider` from `AsyncMapperOverrideMappingOptions` instead of `MapperOverrideMappingOptions`
- Added optional `IServiceProvider` parameter to `EntityFrameworkCoreProjector` to provide `DbContext` instances to project shadow keys during compilation
- Fixed some conditional null checks, which apparently worked even if broken somehow, and managed to pass the tests...
- `EntityFrameworkCoreMatcher` now correctly handles default values for keys (eg: allows to match an entity with a key with value 0)
- Fixed `EntityFrameworkCoreProjector`'s `DbContext` retrieval

## [2.2.0] - 2024-02-03

Expand Down
2 changes: 1 addition & 1 deletion src/NeatMapper.EntityFrameworkCore/Internal/EfCoreUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public static SemaphoreSlim GetOrCreateSemaphoreForDbContext(DbContext dbContext
/// <summary>
/// <see cref="EntityEntry.Property(string)"/>
/// </summary>
public static readonly MethodInfo EntityEntry_Property = typeof(EntityEntry).GetMethod(nameof(EntityEntry.Property))
public static readonly MethodInfo EntityEntry_Property = typeof(EntityEntry).GetMethod(nameof(EntityEntry.Property), new[] { typeof(string) })
?? throw new Exception("Could not find EntityEntry.Property(string)");

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,22 +257,31 @@ public IAsyncNewMapFactory MapAsyncNewFactory(
// Retrieve the db context from the services
var dbContext = RetrieveDbContext(mappingOptions);

var dbContextSemaphore = EfCoreUtils.GetOrCreateSemaphoreForDbContext(dbContext);

var retrievalMode = mappingOptions?.GetOptions<EntityFrameworkCoreMappingOptions>()?.EntitiesRetrievalMode
?? _entityFrameworkCoreOptions.EntitiesRetrievalMode;

var key = _model.FindEntityType(types.To).FindPrimaryKey();
var dbSet = dbContext.GetType().GetMethods().FirstOrDefault(m => m.IsGenericMethod && m.Name == nameof(DbContext.Set)).MakeGenericMethod(types.To).Invoke(dbContext, null)
?? throw new InvalidOperationException("Cannot retrieve DbSet<T>");
var localView = dbSet.GetType().GetProperty(nameof(DbSet<object>.Local)).GetValue(dbSet) as IEnumerable
?? throw new InvalidOperationException("Cannot retrieve DbSet<T>.Local");
object dbSet;
IEnumerable localView;
dbContextSemaphore.Wait();
try {
dbSet = dbContext.GetType().GetMethods().FirstOrDefault(m => m.IsGenericMethod && m.Name == nameof(DbContext.Set)).MakeGenericMethod(types.To).Invoke(dbContext, null)
?? throw new InvalidOperationException("Cannot retrieve DbSet<T>");
localView = dbSet.GetType().GetProperty(nameof(DbSet<object>.Local)).GetValue(dbSet) as IEnumerable
?? throw new InvalidOperationException("Cannot retrieve DbSet<T>.Local");
}
finally {
dbContextSemaphore.Release();
}

var tupleToValueTupleDelegate = types.From.IsTuple() ? EfCoreUtils.GetOrCreateTupleToValueTupleDelegate(types.From) : null;
var keyValuesDelegate = GetOrCreateKeyToValuesDelegate(types.From);

var dbContextSemaphore = EfCoreUtils.GetOrCreateSemaphoreForDbContext(dbContext);

// Create the matcher (it will never throw because of SafeMatcher/EmptyMatcher)
var normalizedElementsMatcherFactory = GetNormalizedMatchFactory(types, mappingOptions);
// Create the matcher used to retrieve local elements (it will never throw because of SafeMatcher/EmptyMatcher), won't contain semaphore
var normalizedElementsMatcherFactory = GetNormalizedMatchFactory(types, mappingOptions
.ReplaceOrAdd<NestedSemaphoreContext>(c => c ?? NestedSemaphoreContext.Instance));
try {
// Check if we are mapping a collection or just a single entity
if (collectionElementTypes != null) {
Expand Down Expand Up @@ -327,6 +336,7 @@ public IAsyncNewMapFactory MapAsyncNewFactory(
missingEntities
.Select(e => keyValuesDelegate.Invoke(e.Key))
.ToArray());
// Locking shouldn't be needed here because Queryable.Where creates just an Expression.Call
var query = Queryable_Where.MakeGenericMethod(types.To).Invoke(null, new object[] { dbSet, filterExpression }) as IQueryable;

await dbContextSemaphore.WaitAsync(cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
using System.Linq.Expressions;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace NeatMapper.EntityFrameworkCore {
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace NeatMapper.EntityFrameworkCore {
/// </summary>
/// <remarks>
/// When working with shadow keys, a <see cref="DbContext"/> will be required.
/// Since a single <see cref="DbContext"/> instance cannot be used concurrently and it is not thread-safe
/// Since a single <see cref="DbContext"/> instance cannot be used concurrently and is not thread-safe
/// on its own, every access to the provided <see cref="DbContext"/> instance and all its members
/// (local and remote) for each match is protected by a semaphore.<br/>
/// This makes this class thread-safe and concurrently usable, though not necessarily efficient to do so.<br/>
Expand All @@ -32,12 +32,12 @@ public sealed class EntityFrameworkCoreMatcher : IMatcher, IMatcherCanMatch, IMa
private readonly IModel _model;

/// <summary>
/// Type of DbContext to retrieve from <see cref="_serviceProvider"/>.
/// Type of DbContext to retrieve from <see cref="_serviceProvider"/>. Used for shadow keys.
/// </summary>
private readonly Type _dbContextType;

/// <summary>
/// Service provider used to retrieve <see cref="DbContext"/> instances.
/// Service provider used to retrieve <see cref="DbContext"/> instances. Used for shadow keys.
/// </summary>
private readonly IServiceProvider _serviceProvider;

Expand Down Expand Up @@ -233,59 +233,41 @@ public IMatchMapFactory MatchFactory(

var entityEntryVar = Expression.Variable(typeof(EntityEntry), "entityEntry");

Expression body;
if (key.Properties.Count == 1) {
// (KeyType)key
var keyExpr = Expression.Convert(keyParam, keyParamType);

if (key.Properties[0].IsShadowProperty()) {
// (KeyType)entityEntry.Property("Id").CurrentValue == KEY
body = Expression.Equal(
Expression.Convert(
var properties = key.Properties
.Select((p, i) => {
if (p.IsShadowProperty()) {
// (KeyItemType)entityEntry.Property("Key1").CurrentValue
return (Expression)Expression.Convert(
Expression.Property(
Expression.Call(
entityEntryVar,
EfCoreUtils.EntityEntry_Property,
Expression.Constant(key.Properties[0].Name)),
Expression.Constant(p.Name)),
EfCoreUtils.MemberEntry_CurrentValue),
keyParamType),
keyExpr);
}
else {
// ((EntityType)entity).Id == KEY
body = Expression.Equal(Expression.Property(Expression.Convert(entityParam, entityType), key.Properties[0].PropertyInfo), keyExpr);
}
p.ClrType);
}
else {
// ((EntityType)entity).Key1
return Expression.PropertyOrField(Expression.Convert(entityParam, entityType), p.Name);
}
});
Expression body;
if (key.Properties.Count == 1) {
// KEYPROP == (KeyType)key
body = Expression.Equal(properties.Single(), Expression.Convert(keyParam, keyParamType));
}
else {
// KEY1 && ...
body = key.Properties
.Select((p, i) => {
// ((KeyType)key).Item1
var keyExpr = Expression.PropertyOrField(Expression.Convert(keyParam, keyParamType), "Item" + (i + 1));

if (p.IsShadowProperty()) {
// (KeyItemType)entityEntry.Property("Key1").CurrentValue == KEY
return Expression.Equal(
Expression.Convert(
Expression.Property(
Expression.Call(
entityEntryVar,
EfCoreUtils.EntityEntry_Property,
Expression.Constant(p.Name)),
EfCoreUtils.MemberEntry_CurrentValue),
p.ClrType),
keyExpr);
}
else {
// ((EntityType)entity).Key1 == KEY
return Expression.Equal(Expression.Property(Expression.Convert(entityParam, entityType), p.PropertyInfo), keyExpr);
}
})
// KEYPROP1 == ((KeyType)key).Item1 && ...
body = properties
.Select((p, i) => Expression.Equal(p, Expression.PropertyOrField(Expression.Convert(keyParam, keyParamType), "Item" + (i + 1))))
.Aggregate(Expression.AndAlso);
}

// If we have a shadow key we must retrieve values from DbContext, so we must use a semaphore (if not already inside one)
// If we have a shadow key we must retrieve values from DbContext, so we must use a semaphore (if not already inside one),
// also we wrap the access to dbContext in a try/catch block to throw map not found if the context is disposed
if (_entityShadowKeyCache.GetOrAdd(entityType, __ => key.Properties.Any(p => p.IsShadowProperty()))) {
var catchExceptionParam = Expression.Parameter(typeof(Exception), "e");

body = Expression.Block(typeof(bool),
// if(dbContextSemaphore != null)
// dbContextSemaphore.Wait()
Expand All @@ -303,14 +285,10 @@ public IMatchMapFactory MatchFactory(
Expression.Equal(Expression.Property(entityEntryVar, EfCoreUtils.EntityEntry_State), Expression.Constant(EntityState.Detached)),
Expression.Throw(
Expression.New(
typeof(MatcherException).GetConstructors().Single(),
Expression.New(
typeof(InvalidOperationException).GetConstructor(new[] { typeof(string) }),
Expression.Constant($"The entity of type {entityType.FullName ?? entityType.Name} is not being tracked " +
$"by the provided {nameof(DbContext)}, so its shadow key(s) cannot be retrieved locally. " +
$"Either provide a valid {nameof(DbContext)} or pass a tracked entity.")),
Expression.Constant((sourceType, destinationType))
),
typeof(InvalidOperationException).GetConstructor(new[] { typeof(string) }),
Expression.Constant($"The entity of type {entityType.FullName ?? entityType.Name} is not being tracked " +
$"by the provided {nameof(DbContext)}, so its shadow key(s) cannot be retrieved locally. " +
$"Either provide a valid {nameof(DbContext)} or pass a tracked entity.")),
body.Type),
body)
),
Expand All @@ -319,7 +297,7 @@ public IMatchMapFactory MatchFactory(
Expression.IfThen(
Expression.NotEqual(dbContextSemaphoreParam, Expression.Constant(null, dbContextSemaphoreParam.Type)),
Expression.Call(dbContextSemaphoreParam, EfCoreUtils.SemaphoreSlim_Release)))
);
);
}

return Expression.Lambda<Func<object, object, SemaphoreSlim, DbContext, bool>>(body, entityParam, keyParam, dbContextSemaphoreParam, dbContextParam).Compile();
Expand Down Expand Up @@ -364,7 +342,21 @@ public IMatchMapFactory MatchFactory(
if (tupleToValueTupleDelegate != null)
keyObject = tupleToValueTupleDelegate.DynamicInvoke(keyObject);

return entityKeyComparer.Invoke(entityObject, keyObject, dbContextSemaphore, dbContext);
try {
return entityKeyComparer.Invoke(entityObject, keyObject, dbContextSemaphore, dbContext);
}
catch (MapNotFoundException e) {
if (e.From == sourceType && e.To == destinationType)
throw;
else
throw new MappingException(e, (sourceType, destinationType));
}
catch (OperationCanceledException) {
throw;
}
catch (Exception e) {
throw new MatcherException(e, (sourceType, destinationType));
}
});
}
else {
Expand Down Expand Up @@ -438,14 +430,10 @@ public IMatchMapFactory MatchFactory(
Expression.Equal(Expression.Property(entityEntry2Var, EfCoreUtils.EntityEntry_State), Expression.Constant(EntityState.Detached))),
Expression.Throw(
Expression.New(
typeof(MatcherException).GetConstructors().Single(),
Expression.New(
typeof(InvalidOperationException).GetConstructor(new[] { typeof(string) }),
Expression.Constant($"The entity(ies) of type {sourceType.FullName ?? sourceType.Name} is/are not being tracked " +
$"by the provided {nameof(DbContext)}, so its/their shadow key(s) cannot be retrieved locally. " +
$"Either provide a valid {nameof(DbContext)} or pass a tracked entity(ies).")),
Expression.Constant((sourceType, destinationType))
),
typeof(InvalidOperationException).GetConstructor(new[] { typeof(string) }),
Expression.Constant($"The entity(ies) of type {sourceType.FullName ?? sourceType.Name} is/are not being tracked " +
$"by the provided {nameof(DbContext)}, so its/their shadow key(s) cannot be retrieved locally. " +
$"Either provide a valid {nameof(DbContext)} or pass a tracked entity(ies).")),
body.Type),
body)
),
Expand Down Expand Up @@ -481,7 +469,21 @@ public IMatchMapFactory MatchFactory(
if (source == null || destination == null)
return false;

return entityEntityComparer.Invoke(source, destination, dbContextSemaphore, dbContext);
try {
return entityEntityComparer.Invoke(source, destination, dbContextSemaphore, dbContext);
}
catch (MapNotFoundException e) {
if (e.From == sourceType && e.To == destinationType)
throw;
else
throw new MappingException(e, (sourceType, destinationType));
}
catch (OperationCanceledException) {
throw;
}
catch (Exception e) {
throw new MatcherException(e, (sourceType, destinationType));
}
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net47;net48;netcoreapp3.1;netstandard2.1;net5.0;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks>net47;net48;netcoreapp3.1;netstandard2.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<Nullable Condition="'$(TargetFramework)' != 'net47' and '$(TargetFramework)' != 'net48'">enable</Nullable>
<PackageId>NeatMapper.EntityFrameworkCore</PackageId>
<Version>2.2.0</Version>
<Version>3.1.0</Version>
<Authors>Xriuk</Authors>
<Title>.NEaT Mapper - Entity Framework Core</Title>
<Description>Creates automatic maps and projections between entities and their keys (even composite and shadow keys), supports normal maps and asynchronous ones, also supports collections (not nested).</Description>
<PackageProjectUrl>https://www.neatmapper.org/ef-core/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Xriuk/NeatMapper/tree/main/src/NeatMapper.EntityFrameworkCore</RepositoryUrl>
<PackageReleaseNotes>See full changelog at https://github.com/Xriuk/NeatMapper/blob/main/src/NeatMapper.EntityFrameworkCore/CHANGELOG.md#220---2024-02-03</PackageReleaseNotes>
<PackageReleaseNotes>See full changelog at https://www.neatmapper.org/ef-core/changelog#310---2024-04-26</PackageReleaseNotes>
<Copyright>Xriuk</Copyright>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand All @@ -25,19 +26,22 @@
<None Remove="..\..\LICENSE.md" />
<None Remove="README.md" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net6.0' Or '$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="[6,8)" />
<ItemGroup Condition="'$(TargetFramework)' == 'net47' Or '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'netcoreapp3.1'">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="[3.1,4)" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.1' Or '$(TargetFramework)' == 'net5.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="[5,6)" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net47' Or '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'netcoreapp3.1'">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="[3.1,4)" />
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0' Or '$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="[6,8)" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="[8,9)" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\NeatMapper\NeatMapper.csproj" PackageVersion="[2.2,3)" />
<ProjectReference Include="..\NeatMapper\NeatMapper.csproj" PackageVersion="[3.1.0,4)" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit c623754

Please sign in to comment.