diff --git a/AppCoreNet.Data.sln b/AppCoreNet.Data.sln
index f1629a1..033ec09 100644
--- a/AppCoreNet.Data.sln
+++ b/AppCoreNet.Data.sln
@@ -48,6 +48,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppCoreNet.Data.MongoDB.Tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppCoreNet.Data.SpecificationTests", "test\AppCoreNet.Data.SpecificationTests\AppCoreNet.Data.SpecificationTests.csproj", "{BFC43033-85FA-479A-AFCE-F7BF70DDB11B}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppCoreNet.Data.EntityFramework", "src\AppCoreNet.Data.EntityFramework\AppCoreNet.Data.EntityFramework.csproj", "{8C3B072A-50E4-4C49-847D-C99999999999}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppCoreNet.Data.EntityFramework.Tests", "test\AppCoreNet.Data.EntityFramework.Tests\AppCoreNet.Data.EntityFramework.Tests.csproj", "{8C3B072A-50E4-4C49-847D-C9999999999A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -90,6 +94,14 @@ Global
{BFC43033-85FA-479A-AFCE-F7BF70DDB11B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFC43033-85FA-479A-AFCE-F7BF70DDB11B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFC43033-85FA-479A-AFCE-F7BF70DDB11B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C3B072A-50E4-4C49-847D-C99999999999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C3B072A-50E4-4C49-847D-C99999999999}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C3B072A-50E4-4C49-847D-C99999999999}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C3B072A-50E4-4C49-847D-C99999999999}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C3B072A-50E4-4C49-847D-C9999999999A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C3B072A-50E4-4C49-847D-C9999999999A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C3B072A-50E4-4C49-847D-C9999999999A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C3B072A-50E4-4C49-847D-C9999999999A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -101,7 +113,9 @@ Global
{2C53E3E5-928F-4D96-8ADF-140F589C1887} = {80A494A8-C591-4A8D-98DF-826D2D32ECA9}
{1CDC4550-43F9-4EEA-B5BB-B32ED68026FA} = {80A494A8-C591-4A8D-98DF-826D2D32ECA9}
{EE52F420-0C7E-4F83-A171-3A1A7B5F5B4F} = {80A494A8-C591-4A8D-98DF-826D2D32ECA9}
+ {8C3B072A-50E4-4C49-847D-C99999999999} = {80A494A8-C591-4A8D-98DF-826D2D32ECA9}
{5E374BA2-6CA6-4EA7-B572-15DFD1DE11EB} = {C8F2D282-96F6-48F4-B707-79E7FF58974C}
+ {8C3B072A-50E4-4C49-847D-C9999999999A} = {C8F2D282-96F6-48F4-B707-79E7FF58974C}
{2F1C1FB9-22F0-4109-85C4-09A59A60465E} = {C8F2D282-96F6-48F4-B707-79E7FF58974C}
{5EC3B837-5244-4C45-8046-D0D7A3C1FBBB} = {80A494A8-C591-4A8D-98DF-826D2D32ECA9}
{AF7F153C-EFE8-4CE5-9882-34B006E8D94F} = {80A494A8-C591-4A8D-98DF-826D2D32ECA9}
diff --git a/README.md b/README.md
index 010a00b..8986139 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ Latest development packages can be found on [MyGet](https://www.myget.org/galler
| `AppCoreNet.Data` | Provides persistence framework default implementations. |
| `AppCoreNet.Data.Abstractions` | Provides the public API of the persistence framework. |
| `AppCoreNet.Data.EntityFrameworkCore` | Adds support for EntityFramework Core. |
+| `AppCoreNet.Data.EntityFramework` | Adds support for EntityFramework 6. |
| `AppCoreNet.Data.MongoDB` | Adds support for Mongo DB. |
| `AppCoreNet.Data.AutoMapper` | Adds support for mapping entities using AutoMapper. |
diff --git a/src/AppCoreNet.Data.EntityFramework/AppCoreNet.Data.EntityFramework.csproj b/src/AppCoreNet.Data.EntityFramework/AppCoreNet.Data.EntityFramework.csproj
new file mode 100644
index 0000000..74702f4
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/AppCoreNet.Data.EntityFramework.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net462;netstandard2.1;net8.0
+ Adds EntityFramework (EF6) support to AppCore .NET persistence.
+ $(PackageTags);EntityFramework;EF6
+ AppCoreNet.Data.EntityFramework
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AppCoreNet.Data.EntityFramework/AppCoreNet.Data.EntityFrameworkCore.csproj b/src/AppCoreNet.Data.EntityFramework/AppCoreNet.Data.EntityFrameworkCore.csproj
new file mode 100644
index 0000000..a442802
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/AppCoreNet.Data.EntityFrameworkCore.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net8.0
+ Adds EntityFramework Core support to AppCore .NET persistence.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AppCoreNet.Data.EntityFramework/DbContextDataProvider.cs b/src/AppCoreNet.Data.EntityFramework/DbContextDataProvider.cs
new file mode 100644
index 0000000..e67d862
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DbContextDataProvider.cs
@@ -0,0 +1,102 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System.Data.Entity;
+using System.Data.Entity.Infrastructure;
+using System.Threading;
+using System.Threading.Tasks;
+using AppCoreNet.Diagnostics;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Represents a Entity Framework data provider.
+///
+/// The type of the .
+public sealed class DbContextDataProvider : IDataProvider
+ where TDbContext : DbContext
+{
+ private readonly string _name;
+ private readonly DbContextDataProviderServices _services;
+
+ ///
+ public string Name => _name;
+
+ ///
+ /// Gets the of the data provider.
+ ///
+ /// The .
+ public TDbContext DbContext => _services.DbContext;
+
+ ///
+ /// Gets the of the data provider.
+ ///
+ public IEntityMapper EntityMapper => _services.EntityMapper;
+
+ ///
+ /// Gets the of the data provider.
+ ///
+ public ITokenGenerator TokenGenerator => _services.TokenGenerator;
+
+ ///
+ /// Gets the of the data provider.
+ ///
+ public DbContextQueryHandlerFactory QueryHandlerFactory => _services.QueryHandlerFactory;
+
+ ///
+ /// Gets the .
+ ///
+ public DbContextTransactionManager TransactionManager => _services.TransactionManager;
+
+ ITransactionManager IDataProvider.TransactionManager => TransactionManager;
+
+ internal DataProviderLogger> Logger => _services.Logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the data provider.
+ /// The .
+ public DbContextDataProvider(
+ string name,
+ DbContextDataProviderServices services)
+ {
+ Ensure.Arg.NotNull(name);
+ Ensure.Arg.NotNull(services);
+
+ _name = name;
+ _services = services;
+ }
+
+ ///
+ /// Saves changes made to the .
+ ///
+ /// Optional .
+ /// A representing the asynchronous operation.
+ public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ // EF6 SaveChangesAsync returns Task
+ await DbContext.SaveChangesAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (DbUpdateConcurrencyException error)
+ {
+ throw new EntityConcurrencyException(error);
+ }
+ catch (DbUpdateException error)
+ {
+ throw new EntityUpdateException(error);
+ }
+
+ // detach all entities after saving changes
+ foreach (DbEntityEntry entry in DbContext.ChangeTracker.Entries())
+ {
+ if (entry.Entity != null)
+ {
+ entry.State = EntityState.Detached;
+ }
+ }
+ }
+}
diff --git a/src/AppCoreNet.Data.EntityFramework/DbContextDataProviderOptions.cs b/src/AppCoreNet.Data.EntityFramework/DbContextDataProviderOptions.cs
new file mode 100644
index 0000000..db22bde
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DbContextDataProviderOptions.cs
@@ -0,0 +1,16 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System;
+using System.Collections.Generic;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+internal sealed class DbContextDataProviderOptions
+{
+ public Type? EntityMapperType { get; set; }
+
+ public Type? TokenGeneratorType { get; set; }
+
+ public ISet QueryHandlerTypes { get; } = new HashSet();
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/DbContextDataProviderServices.cs b/src/AppCoreNet.Data.EntityFramework/DbContextDataProviderServices.cs
new file mode 100644
index 0000000..fb24faf
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DbContextDataProviderServices.cs
@@ -0,0 +1,94 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System;
+using System.Data.Entity;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Provides services for based data provider.
+///
+/// The type of the .
+public sealed class DbContextDataProviderServices
+ where TDbContext : DbContext
+{
+ ///
+ /// Gets the .
+ ///
+ public TDbContext DbContext { get; }
+
+ ///
+ /// Gets the .
+ ///
+ public IEntityMapper EntityMapper { get; }
+
+ ///
+ /// Gets the .
+ ///
+ public ITokenGenerator TokenGenerator { get; }
+
+ ///
+ /// Gets the .
+ ///
+ public DbContextQueryHandlerFactory QueryHandlerFactory { get; }
+
+ ///
+ /// Gets the .
+ ///
+ public DbContextTransactionManager TransactionManager { get; }
+
+ ///
+ /// Gets the .
+ ///
+ public DataProviderLogger> Logger { get; }
+
+ internal DbContextDataProviderServices(
+ TDbContext dbContext,
+ IEntityMapper entityMapper,
+ ITokenGenerator tokenGenerator,
+ DbContextQueryHandlerFactory queryHandlerFactory,
+ DbContextTransactionManager transactionManager,
+ DataProviderLogger> logger)
+ {
+ DbContext = dbContext;
+ EntityMapper = entityMapper;
+ TokenGenerator = tokenGenerator;
+ QueryHandlerFactory = queryHandlerFactory;
+ TransactionManager = transactionManager;
+ Logger = logger;
+ }
+
+ private static T GetOrCreateInstance(IServiceProvider serviceProvider, Type? type)
+ where T : notnull
+ {
+ return type != null
+ ? (T)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, type)
+ : serviceProvider.GetRequiredService();
+ }
+
+ internal static DbContextDataProviderServices Create(string name, IServiceProvider serviceProvider)
+ {
+ var optionsMonitor = serviceProvider.GetRequiredService>();
+ DbContextDataProviderOptions options = optionsMonitor.Get(name);
+
+ var entityMapper = GetOrCreateInstance(serviceProvider, options.EntityMapperType);
+ var tokenGenerator = GetOrCreateInstance(serviceProvider, options.TokenGeneratorType);
+ var logger = serviceProvider.GetRequiredService>>();
+
+ var dbContext = serviceProvider.GetRequiredService();
+ var queryHandlerFactory = new DbContextQueryHandlerFactory(serviceProvider, options.QueryHandlerTypes);
+ var transactionManager = new DbContextTransactionManager(dbContext, logger);
+
+ return new DbContextDataProviderServices(
+ dbContext,
+ entityMapper,
+ tokenGenerator,
+ queryHandlerFactory,
+ transactionManager,
+ logger);
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/DbContextPagedQueryHandler.cs b/src/AppCoreNet.Data.EntityFramework/DbContextPagedQueryHandler.cs
new file mode 100644
index 0000000..64bc85c
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DbContextPagedQueryHandler.cs
@@ -0,0 +1,86 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System.Data.Entity;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Provides a base class for based query handlers which return a page of the result set.
+///
+/// The type of the .
+/// The type of the .
+/// The type of the result.
+/// The type of the .
+/// The type of the DB entity.
+public abstract class DbContextPagedQueryHandler
+ : DbContextQueryHandler, TDbContext, TDbEntity>
+ where TQuery : IPagedQuery
+ where TEntity : class, IEntity
+ where TDbContext : DbContext
+ where TDbEntity : class
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ protected DbContextPagedQueryHandler(DbContextDataProvider provider)
+ : base(provider)
+ {
+ }
+
+ ///
+ /// Must be implemented to apply the query to the .
+ ///
+ /// The .
+ /// The to apply.
+ /// The with the query applied.
+ protected abstract IQueryable ApplyQuery(IQueryable queryable, TQuery query);
+
+ ///
+ /// Must be implemented to project the result from to .
+ ///
+ /// The which must be projected.
+ /// The which is executed.
+ /// The projected .
+ protected abstract IQueryable ApplyProjection(IQueryable queryable, TQuery query);
+
+ ///
+ /// Can be overriden to customize the paging.
+ ///
+ /// The which must be paged.
+ /// The which is executed.
+ /// The projected .
+ protected virtual IQueryable ApplyPaging(IQueryable queryable, TQuery query)
+ {
+ if (query.Offset > 0)
+ queryable = queryable.Skip((int)query.Offset);
+
+ return queryable.Take(query.Limit);
+ }
+
+ ///
+ protected override async Task> QueryResultAsync(
+ IQueryable queryable,
+ TQuery query,
+ CancellationToken cancellationToken)
+ {
+ queryable = ApplyQuery(queryable, query);
+
+ long? totalCount = query.TotalCount
+ ? await queryable.LongCountAsync(cancellationToken)
+ .ConfigureAwait(false)
+ : null;
+
+ queryable = ApplyPaging(queryable, query);
+ IQueryable projectedQueryable = ApplyProjection(queryable, query);
+
+ TResult[] result = await projectedQueryable.ToArrayAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ return new PagedResult(result, totalCount);
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/DbContextQueryHandler.cs b/src/AppCoreNet.Data.EntityFramework/DbContextQueryHandler.cs
new file mode 100644
index 0000000..3c2ae43
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DbContextQueryHandler.cs
@@ -0,0 +1,106 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System.Data.Entity;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AppCoreNet.Diagnostics;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Provides a base class for based query handlers.
+///
+/// The type of the .
+/// The type of the .
+/// The type of the result.
+/// The type of the .
+/// The type of the DB entity.
+public abstract class DbContextQueryHandler
+ : IDbContextQueryHandler
+ where TQuery : IQuery
+ where TEntity : class, IEntity
+ where TDbContext : DbContext
+ where TDbEntity : class
+{
+ ///
+ /// Gets the used by the query.
+ ///
+ protected DbContextDataProvider Provider { get; }
+
+ ///
+ /// Initializes a new instance of the
+ /// class.
+ ///
+ /// The .
+ protected DbContextQueryHandler(DbContextDataProvider provider)
+ {
+ Ensure.Arg.NotNull(provider);
+ Provider = provider;
+ }
+
+ ///
+ /// Gets the of the database entity.
+ ///
+ /// The query.
+ /// Token which can be used to cancel the process.
+ /// The queryable.
+ protected virtual ValueTask> GetQueryableAsync(
+ TQuery query,
+ CancellationToken cancellationToken)
+ {
+ IQueryable queryable =
+ Provider.DbContext
+ .Set()
+ .AsNoTracking();
+
+ return new ValueTask>(queryable);
+ }
+
+ ///
+ /// Must be implemented to query the result.
+ ///
+ /// The which must be queried.
+ /// The for which results must be queried.
+ /// Token which can be used to cancel the process.
+ /// The result of the query.
+ protected abstract Task QueryResultAsync(
+ IQueryable queryable,
+ TQuery query,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Executes the given query.
+ ///
+ /// The query to execute.
+ /// Token which can be used to cancel the process.
+ /// The result of the query.
+ public virtual async Task ExecuteAsync(TQuery query, CancellationToken cancellationToken = default)
+ {
+ Ensure.Arg.NotNull(query);
+
+ IQueryable queryable = await GetQueryableAsync(query, cancellationToken)
+ .ConfigureAwait(false);
+
+ TResult result = await QueryResultAsync(queryable, query, cancellationToken)
+ .ConfigureAwait(false);
+
+ return result;
+ }
+
+ ///
+ bool IDbContextQueryHandler.CanExecute(IQuery query)
+ {
+ return query is TQuery;
+ }
+
+ ///
+ Task IDbContextQueryHandler.ExecuteAsync(
+ IQuery query,
+ CancellationToken cancellationToken)
+ {
+ Ensure.Arg.NotNull(query);
+ return ExecuteAsync((TQuery)query, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/DbContextQueryHandlerFactory.cs b/src/AppCoreNet.Data.EntityFramework/DbContextQueryHandlerFactory.cs
new file mode 100644
index 0000000..5642bee
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DbContextQueryHandlerFactory.cs
@@ -0,0 +1,74 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System;
+using System.Collections.Generic;
+using System.Data.Entity;
+using System.Linq;
+using AppCoreNet.Diagnostics;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Provides query handler factory for .
+///
+/// The type of the .
+public sealed class DbContextQueryHandlerFactory
+ where TDbContext : DbContext
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IEnumerable _queryHandlerTypes;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ /// The types of the query handlers.
+ public DbContextQueryHandlerFactory(IServiceProvider serviceProvider, IEnumerable queryHandlerTypes)
+ {
+ Ensure.Arg.NotNull(serviceProvider);
+ Ensure.Arg.NotNull(queryHandlerTypes);
+
+ _serviceProvider = serviceProvider;
+ _queryHandlerTypes = queryHandlerTypes;
+ }
+
+ ///
+ /// Creates a query handler for the specified query.
+ ///
+ /// The .
+ /// The query.
+ /// The type of the .
+ /// The type of the result.
+ /// The .
+ /// There is no handler registered for the specified query.
+ public IDbContextQueryHandler CreateHandler(
+ DbContextDataProvider provider,
+ IQuery query)
+ where TEntity : class, IEntity
+ {
+ Ensure.Arg.NotNull(provider);
+ Ensure.Arg.NotNull(query);
+
+ Type queryHandlerType = typeof(IDbContextQueryHandler);
+
+ IEnumerable eligibleHandlers = _queryHandlerTypes.Where(
+ t => queryHandlerType.IsAssignableFrom(t));
+
+ foreach (Type handlerType in eligibleHandlers)
+ {
+ var handler =
+ (IDbContextQueryHandler)ActivatorUtilities.CreateInstance(
+ _serviceProvider,
+ handlerType,
+ provider);
+
+ if (handler.CanExecute(query))
+ return handler;
+ }
+
+ throw new InvalidOperationException(
+ $"There is no handler for query type '{query.GetType().GetDisplayName()}' registered.");
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/DbContextRepository.cs b/src/AppCoreNet.Data.EntityFramework/DbContextRepository.cs
new file mode 100644
index 0000000..0217eef
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DbContextRepository.cs
@@ -0,0 +1,570 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System;
+using System.Collections.Generic;
+using System.Data.Entity;
+using System.Data.Entity.Infrastructure;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading;
+using System.Threading.Tasks;
+using AppCoreNet.Diagnostics;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Provides a based implementation of the interface.
+///
+/// The type of the entity id.
+/// The type of the entity.
+/// The type of the .
+/// The type of the database entity.
+public class DbContextRepository : IDbContextRepository, IRepository
+ where TEntity : class, IEntity
+ where TDbContext : DbContext
+ where TDbEntity : class
+{
+ ///
+ /// Provides a base class for scalar query handlers of this repository.
+ ///
+ /// The type of the query.
+ /// The type of the result.
+ public abstract class ScalarQueryHandler : DbContextScalarQueryHandler
+ where TQuery : IQuery
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ protected ScalarQueryHandler(DbContextDataProvider provider)
+ : base(provider)
+ {
+ }
+ }
+
+ ///
+ /// Provides a base class for vector query handlers of this repository.
+ ///
+ /// The type of the query.
+ /// The type of the result.
+ public abstract class VectorQueryHandler : DbContextVectorQueryHandler
+ where TQuery : IQuery>
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ protected VectorQueryHandler(DbContextDataProvider provider)
+ : base(provider)
+ {
+ }
+ }
+
+ ///
+ /// Provides a base class for paged query handlers of this repository.
+ ///
+ /// The type of the query.
+ /// The type of the result.
+ public abstract class PagedQueryHandler : DbContextPagedQueryHandler
+ where TQuery : IPagedQuery
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ protected PagedQueryHandler(DbContextDataProvider provider)
+ : base(provider)
+ {
+ }
+ }
+
+ private static readonly EntityModelProperties _entityModelProperties = new();
+ private readonly DbModelProperties _modelProperties;
+
+ ///
+ public DbContextDataProvider Provider { get; }
+
+ ///
+ /// Gets the .
+ ///
+ protected DbSet Set => Provider.DbContext.Set();
+
+ IDataProvider IRepository.Provider => Provider;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The data provider.
+ public DbContextRepository(DbContextDataProvider provider)
+ {
+ Ensure.Arg.NotNull(provider);
+ Provider = provider;
+ _modelProperties = DbModelProperties.Get(provider.DbContext, typeof(TDbEntity), typeof(TEntity));
+ }
+
+ ///
+ /// Can be overridden to get the primary key from the specified entity id.
+ ///
+ /// The unique entity id.
+ /// The primary key values.
+ protected virtual object?[] GetPrimaryKey(TId id)
+ {
+ return _entityModelProperties.GetIdValues(id);
+ }
+
+ ///
+ /// Can be overridden to apply the query expression used when searching for entities by
+ /// it's primary key.
+ ///
+ /// The queryable.
+ /// The unique entity id.
+ /// The queryable filtered by primary key.
+ protected virtual IQueryable ApplyPrimaryKeyExpression(IQueryable queryable, TId id)
+ {
+ object?[] primaryKeyValues = GetPrimaryKey(id);
+ ParameterExpression parameter = Expression.Parameter(typeof(TDbEntity), "e");
+
+ Expression? predicate = null;
+ for (int i = 0; i < _modelProperties.PrimaryKeyPropertyNames.Count; i++)
+ {
+ string pkPropertyName = _modelProperties.PrimaryKeyPropertyNames[i];
+ object? pkValue = primaryKeyValues[i];
+
+ Expression property = Expression.Property(parameter, pkPropertyName);
+ Expression value = Expression.Constant(pkValue);
+ Expression equals = Expression.Equal(property, Expression.Convert(value, property.Type));
+
+ predicate = predicate == null ? equals : Expression.AndAlso(predicate, equals);
+ }
+
+ if (predicate == null)
+ return queryable;
+
+ return queryable.Where(Expression.Lambda>(predicate, parameter));
+ }
+
+ ///
+ /// Can be overridden to apply includes to the query.
+ ///
+ /// The .
+ /// The queryable.
+ protected virtual IQueryable ApplyIncludes(IQueryable queryable)
+ {
+ return queryable;
+ }
+
+ private void UpdateChangeToken(DbEntityEntry entry, TEntity entity)
+ {
+ if (_modelProperties.HasConcurrencyToken)
+ {
+ DbPropertyEntry concurrencyTokenProperty =
+ entry.Property(_modelProperties.ConcurrencyTokenPropertyName!);
+
+ if (entity is IHasChangeTokenEx expectedChangeToken)
+ {
+ if (entry.State != EntityState.Added)
+ {
+ concurrencyTokenProperty.OriginalValue = expectedChangeToken.ExpectedChangeToken;
+ }
+
+ if (entry.State != EntityState.Deleted)
+ {
+ string? changeToken = expectedChangeToken.ChangeToken;
+ if (string.IsNullOrWhiteSpace(changeToken)
+ || string.Equals(changeToken, expectedChangeToken.ExpectedChangeToken))
+ {
+ changeToken = Provider.TokenGenerator.Generate();
+ }
+
+ concurrencyTokenProperty.CurrentValue = changeToken;
+ }
+ }
+ else if (entity is IHasChangeToken changeToken)
+ {
+ if (entry.State != EntityState.Added)
+ {
+ concurrencyTokenProperty.OriginalValue = changeToken.ChangeToken;
+ }
+
+ if (entry.State != EntityState.Deleted)
+ {
+ concurrencyTokenProperty.CurrentValue = Provider.TokenGenerator.Generate();
+ }
+ }
+ }
+ }
+
+ private IQueryable GetQueryable(TId id)
+ {
+ return ApplyPrimaryKeyExpression(ApplyIncludes(Set), id);
+ }
+
+ private async Task DoAsync(Action before, Func> action, Action after, Action failed)
+ {
+ before();
+
+ var stopwatch = new Stopwatch();
+ stopwatch.Start();
+
+ try
+ {
+ T result = await action()
+ .ConfigureAwait(false);
+
+ after(result, stopwatch.ElapsedMilliseconds);
+
+ return result;
+ }
+ catch (Exception error)
+ {
+ failed(error);
+ throw;
+ }
+ }
+
+ private async Task DoAsync(Action before, Func action, Action after, Action failed)
+ {
+ await DoAsync(
+ before,
+ async () =>
+ {
+ await action()
+ .ConfigureAwait(false);
+
+ return true;
+ },
+ (_, elapsedTimeMs) => after(elapsedTimeMs),
+ failed);
+ }
+
+ ///
+ public async Task QueryAsync(
+ IQuery query,
+ CancellationToken cancellationToken = default)
+ {
+ Ensure.Arg.NotNull(query);
+
+ IDbContextQueryHandler queryHandler =
+ Provider.QueryHandlerFactory.CreateHandler(Provider, query);
+
+ TResult result;
+ try
+ {
+ result =
+ await DoAsync(
+ () =>
+ {
+ Provider.Logger.QueryExecuting(query);
+ },
+ async () =>
+ {
+ return await queryHandler.ExecuteAsync(query, cancellationToken)
+ .ConfigureAwait(false);
+ },
+ (_, elapsedTimeMs) =>
+ {
+ Provider.Logger.QueryExecuted(query, elapsedTimeMs);
+ },
+ error =>
+ {
+ Provider.Logger.QueryExecuteFailed(error, query);
+ })
+ .ConfigureAwait(false);
+ }
+ finally
+ {
+ await DisposeQueryHandlerAsync(queryHandler)
+ .ConfigureAwait(false);
+ }
+
+ [SuppressMessage(
+ "IDisposableAnalyzers.Correctness",
+ "IDISP007:Don\'t dispose injected",
+ Justification = "Ownership is correct.")]
+ [SuppressMessage(
+ "ReSharper",
+ "SuspiciousTypeConversion.Global",
+ Justification = "Handler may implement IDisposable.")]
+ async ValueTask DisposeQueryHandlerAsync(IDbContextQueryHandler handler)
+ {
+ switch (handler)
+ {
+ case IAsyncDisposable disposable:
+ await disposable.DisposeAsync()
+ .ConfigureAwait(false);
+ break;
+ case IDisposable disposable:
+ disposable.Dispose();
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Provides the core logic to find an entity by it's unique identifier.
+ ///
+ /// The entity identifier.
+ /// Optional .
+ /// The entity if found, otherwise null.
+ protected virtual async Task FindCoreAsync(TId id, CancellationToken cancellationToken)
+ {
+ TDbEntity? dbEntity = await GetQueryable(id)
+ .AsNoTracking()
+ .FirstOrDefaultAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ return dbEntity;
+ }
+
+ ///
+ public virtual async Task FindAsync(TId id, CancellationToken cancellationToken = default)
+ {
+ Ensure.Arg.NotNull(id);
+
+ TEntity? entity =
+ await DoAsync(
+ () =>
+ {
+ Provider.Logger.EntityLoading(typeof(TEntity), id);
+ },
+ async () =>
+ {
+ TDbEntity? dbEntity = await FindCoreAsync(id, cancellationToken)
+ .ConfigureAwait(false);
+
+ return dbEntity != null
+ ? Provider.EntityMapper.Map(dbEntity)
+ : default;
+ },
+ (result, elapsedTimeMs) =>
+ {
+ if (result != default)
+ {
+ Provider.Logger.EntityLoaded(result, elapsedTimeMs);
+ }
+ else
+ {
+ Provider.Logger.EntityNotFound(typeof(TEntity), id);
+ }
+ },
+ error =>
+ {
+ Provider.Logger.EntityLoadFailed(error, typeof(TEntity), id);
+ })
+ .ConfigureAwait(false);
+
+ return entity;
+ }
+
+ ///
+ public virtual async Task LoadAsync(TId id, CancellationToken cancellationToken = default)
+ {
+ TEntity? entity = await FindAsync(id, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (entity == null)
+ {
+ throw new EntityNotFoundException(typeof(TEntity), id!);
+ }
+
+ return entity;
+ }
+
+ ///
+ /// Provides the core logic to create an entity.
+ ///
+ /// The entity.
+ /// The database entity.
+ /// Optional .
+ /// The created entity.
+ protected virtual ValueTask> CreateCoreAsync(
+ TEntity entity,
+ TDbEntity dbEntity,
+ CancellationToken cancellationToken)
+ {
+ TDbEntity addedDbEntity = Set.Add(dbEntity);
+ return new ValueTask>(Provider.DbContext.Entry(addedDbEntity));
+ }
+
+ ///
+ public virtual async Task CreateAsync(TEntity entity, CancellationToken cancellationToken = default)
+ {
+ Ensure.Arg.NotNull(entity);
+
+ TEntity result =
+ await DoAsync(
+ () =>
+ {
+ Provider.Logger.EntityCreating(entity);
+ },
+ async () =>
+ {
+ var dbEntity = Provider.EntityMapper.Map(entity);
+
+ DbEntityEntry dbEntry = await CreateCoreAsync(entity, dbEntity, cancellationToken)
+ .ConfigureAwait(false);
+
+ dbEntity = dbEntry.Entity;
+ UpdateChangeToken(dbEntry, entity);
+
+ await Provider.SaveChangesAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ return Provider.EntityMapper.Map(dbEntity);
+ },
+ (e, elapsedTimeMs) =>
+ {
+ Provider.Logger.EntityCreated(e, elapsedTimeMs);
+ },
+ error =>
+ {
+ Provider.Logger.EntityCreateFailed(error, entity);
+ })
+ .ConfigureAwait(false);
+
+ return result;
+ }
+
+ ///
+ /// Provides the core logic to update an entity.
+ ///
+ /// The entity.
+ /// The database entity.
+ /// Optional .
+ /// The updated entity.
+ protected virtual ValueTask> UpdateCoreAsync(
+ TEntity entity,
+ TDbEntity dbEntity,
+ CancellationToken cancellationToken)
+ {
+ // This method is called from UpdateAsync where dbEntity is loaded from the context
+ // and then mapped. So, it should be tracked.
+ // If it were detached for some reason, attaching and setting state is correct.
+ DbEntityEntry entry = Provider.DbContext.Entry(dbEntity);
+ if (entry.State == EntityState.Detached)
+ {
+ Set.Attach(dbEntity);
+ entry = Provider.DbContext.Entry(dbEntity);
+ }
+
+ entry.State = EntityState.Modified;
+ return new ValueTask>(entry);
+ }
+
+ ///
+ public virtual async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
+ {
+ Ensure.Arg.NotNull(entity);
+
+ if (entity.IsTransient())
+ {
+ throw new InvalidOperationException(
+ $"The entity cannot be updated because the '{nameof(IEntity.Id)}' property has the default value.");
+ }
+
+ TEntity result =
+ await DoAsync(
+ () =>
+ {
+ Provider.Logger.EntityUpdating(entity);
+ },
+ async () =>
+ {
+ TDbEntity? dbEntity = await GetQueryable(entity.Id)
+ .FirstOrDefaultAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ if (dbEntity == null)
+ throw new EntityConcurrencyException();
+
+ Provider.EntityMapper.Map(entity, dbEntity);
+ Provider.DbContext.Entry(dbEntity).State = EntityState.Modified;
+
+ DbEntityEntry dbEntry = await UpdateCoreAsync(
+ entity,
+ dbEntity,
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ dbEntity = dbEntry.Entity;
+ UpdateChangeToken(dbEntry, entity);
+
+ await Provider.SaveChangesAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ return Provider.EntityMapper.Map(dbEntity);
+ },
+ (e, elapsedTimeMs) =>
+ {
+ Provider.Logger.EntityUpdated(e, elapsedTimeMs);
+ },
+ error =>
+ {
+ Provider.Logger.EntityUpdateFailed(error, entity);
+ })
+ .ConfigureAwait(false);
+
+ return result;
+ }
+
+ ///
+ /// Provides the core logic to delete an entity.
+ ///
+ /// The entity.
+ /// The database entity.
+ /// Optional .
+ /// The deleted entity.
+ protected virtual ValueTask> DeleteCoreAsync(
+ TEntity entity,
+ TDbEntity dbEntity,
+ CancellationToken cancellationToken)
+ {
+ DbEntityEntry entry = Provider.DbContext.Entry(dbEntity);
+ if (entry.State == EntityState.Detached)
+ {
+ Set.Attach(dbEntity);
+ entry = Provider.DbContext.Entry(dbEntity);
+ }
+
+ TDbEntity removedEntity = Set.Remove(dbEntity);
+ return new ValueTask>(Provider.DbContext.Entry(removedEntity));
+ }
+
+ ///
+ public virtual async Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default)
+ {
+ Ensure.Arg.NotNull(entity);
+
+ await DoAsync(
+ () =>
+ {
+ Provider.Logger.EntityDeleting(entity);
+ },
+ async () =>
+ {
+ var dbEntity = Provider.EntityMapper.Map(entity);
+
+ DbEntityEntry dbEntry = await DeleteCoreAsync(entity, dbEntity, cancellationToken)
+ .ConfigureAwait(false);
+
+ UpdateChangeToken(dbEntry, entity);
+
+ await Provider.SaveChangesAsync(cancellationToken)
+ .ConfigureAwait(false);
+ },
+ elapsedTimeMs =>
+ {
+ Provider.Logger.EntityDeleted(entity, elapsedTimeMs);
+ },
+ error =>
+ {
+ Provider.Logger.EntityDeleteFailed(error, entity);
+ })
+ .ConfigureAwait(false);
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/DbContextScalarQueryHandler.cs b/src/AppCoreNet.Data.EntityFramework/DbContextScalarQueryHandler.cs
new file mode 100644
index 0000000..1bceba9
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DbContextScalarQueryHandler.cs
@@ -0,0 +1,62 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System.Data.Entity;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Provides a base class for based query handlers which return a scalar.
+///
+/// The type of the .
+/// The type of the .
+/// The type of the result.
+/// The type of the .
+/// The type of the DB entity.
+public abstract class DbContextScalarQueryHandler
+ : DbContextQueryHandler
+ where TQuery : IQuery
+ where TEntity : class, IEntity
+ where TDbContext : DbContext
+ where TDbEntity : class
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ protected DbContextScalarQueryHandler(DbContextDataProvider provider)
+ : base(provider)
+ {
+ }
+
+ ///
+ /// Must be implemented to apply the query to the .
+ ///
+ /// The .
+ /// The to apply.
+ /// The with applied query.
+ protected abstract IQueryable ApplyQuery(IQueryable queryable, TQuery query);
+
+ ///
+ /// Must be implemented to project the result from to .
+ ///
+ /// The which must be projected.
+ /// The which is executed.
+ /// The projected .
+ protected abstract IQueryable ApplyProjection(IQueryable queryable, TQuery query);
+
+ ///
+ protected override async Task QueryResultAsync(
+ IQueryable queryable,
+ TQuery query,
+ CancellationToken cancellationToken)
+ {
+ queryable = ApplyQuery(queryable, query);
+ IQueryable result = ApplyProjection(queryable, query);
+ return await result.FirstOrDefaultAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/DbContextTransaction.cs b/src/AppCoreNet.Data.EntityFramework/DbContextTransaction.cs
new file mode 100644
index 0000000..e0104f3
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DbContextTransaction.cs
@@ -0,0 +1,133 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System;
+using System.Data.Entity;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using AppCoreNet.Diagnostics;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Provides a transaction scope.
+///
+/// The type of the .
+[SuppressMessage(
+ "IDisposableAnalyzers.Correctness",
+ "IDISP007:Don\'t dispose injected",
+ Justification = "Ownership is transferred from DbContextTransactionManager.")]
+public sealed class DbContextTransaction : ITransaction
+ where TDbContext : DbContext
+{
+ private readonly TDbContext _dbContext;
+ private readonly DbContextTransaction _transaction;
+ private readonly DataProviderLogger> _logger;
+ private bool _disposed;
+
+ ///
+ public string Id => _transaction.GetHashCode().ToString("X");
+
+ internal DbContextTransaction Transaction => _transaction;
+
+ internal event EventHandler? TransactionFinished;
+
+ internal DbContextTransaction(
+ TDbContext dbContext,
+ DbContextTransaction transaction,
+ DataProviderLogger> logger)
+ {
+ Ensure.Arg.NotNull(dbContext);
+ Ensure.Arg.NotNull(transaction);
+ Ensure.Arg.NotNull(logger);
+
+ _dbContext = dbContext;
+ _transaction = transaction;
+ _logger = logger;
+ _logger.TransactionCreated(this);
+ }
+
+ private void EnsureNotDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(DbContextTransaction));
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+
+ _transaction.Dispose();
+ _disposed = true;
+ _logger.TransactionDisposed(this);
+
+ TransactionFinished?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ Dispose();
+ await Task.CompletedTask;
+ }
+
+ ///
+ public void Commit()
+ {
+ EnsureNotDisposed();
+ _logger.TransactionCommitting(this);
+ var stopwatch = Stopwatch.StartNew();
+ try
+ {
+ _transaction.Commit();
+ _logger.TransactionCommitted(this, stopwatch.ElapsedMilliseconds);
+ }
+ catch (Exception error)
+ {
+ _logger.TransactionCommitFailed(this, error);
+ throw;
+ }
+
+ TransactionFinished?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ public Task CommitAsync(CancellationToken cancellationToken = default)
+ {
+ Commit();
+ return Task.CompletedTask;
+ }
+
+ ///
+ public void Rollback()
+ {
+ EnsureNotDisposed();
+ _logger.TransactionRollingback(this);
+ var stopwatch = Stopwatch.StartNew();
+ try
+ {
+ _transaction.Rollback();
+ _logger.TransactionRolledback(this, stopwatch.ElapsedMilliseconds);
+ }
+ catch (Exception error)
+ {
+ _logger.TransactionRollbackFailed(this, error);
+ throw;
+ }
+
+ TransactionFinished?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ public Task RollbackAsync(CancellationToken cancellationToken = default)
+ {
+ Rollback();
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/DbContextTransactionManager.cs b/src/AppCoreNet.Data.EntityFramework/DbContextTransactionManager.cs
new file mode 100644
index 0000000..e7169b1
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DbContextTransactionManager.cs
@@ -0,0 +1,114 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System;
+using System.Data;
+using System.Data.Entity;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using AppCoreNet.Diagnostics;
+using Microsoft.Extensions.Logging;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Provides a transaction manager using a .
+///
+/// The type of the .
+[SuppressMessage(
+ "IDisposableAnalyzers.Correctness",
+ "IDISP003:Dispose previous before re-assigning",
+ Justification = "Pre-condition is that no transaction is active.")]
+[SuppressMessage(
+ "IDisposableAnalyzers.Correctness",
+ "IDISP006:Implement IDisposable",
+ Justification = "Transaction must be disposed by consumer.")]
+public sealed class DbContextTransactionManager : ITransactionManager
+ where TDbContext : DbContext
+{
+ private readonly TDbContext _dbContext;
+ private readonly DataProviderLogger> _logger;
+ private DbContextTransaction? _currentTransaction;
+
+ ///
+ /// Gets the currently active .
+ ///
+ public DbContextTransaction? CurrentTransaction => _currentTransaction;
+
+ ITransaction? ITransactionManager.CurrentTransaction => CurrentTransaction;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ /// The .
+ public DbContextTransactionManager(
+ TDbContext dbContext,
+ DataProviderLogger> logger)
+ {
+ Ensure.Arg.NotNull(dbContext);
+ Ensure.Arg.NotNull(logger);
+
+ _dbContext = dbContext;
+ _logger = logger;
+ }
+
+ private void OnTransactionFinished(object? sender, EventArgs args)
+ {
+ var transaction = (DbContextTransaction)sender!;
+ transaction.TransactionFinished -= OnTransactionFinished;
+ _currentTransaction = null;
+ }
+
+ ///
+ /// Begins a new transaction in the context of the data provider.
+ ///
+ /// Specifies the isolation level of the transaction.
+ /// Can be used to cancel the asynchronous operation.
+ /// The created transaction.
+ public Task BeginTransactionAsync(
+ IsolationLevel isolationLevel,
+ CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(BeginTransaction(isolationLevel));
+ }
+
+ ///
+ public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)
+ {
+ return await BeginTransactionAsync(IsolationLevel.Unspecified, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Begins a new transaction in the context of the data provider.
+ ///
+ /// Specifies the isolation level of the transaction.
+ /// The created transaction.
+ public ITransaction BeginTransaction(IsolationLevel isolationLevel)
+ {
+ if (CurrentTransaction != null)
+ throw new InvalidOperationException("A transaction is already in progress.");
+
+ DbContextTransaction transaction = _dbContext.Database.BeginTransaction(isolationLevel);
+
+ try
+ {
+ var t = new DbContextTransaction(_dbContext, transaction, _logger);
+ t.TransactionFinished += OnTransactionFinished;
+ return _currentTransaction = t;
+ }
+ catch
+ {
+ transaction.Dispose();
+ throw;
+ }
+ }
+
+ ///
+ public ITransaction BeginTransaction()
+ {
+ return BeginTransaction(IsolationLevel.Unspecified);
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/DbContextVectorQueryHandler.cs b/src/AppCoreNet.Data.EntityFramework/DbContextVectorQueryHandler.cs
new file mode 100644
index 0000000..a55624c
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DbContextVectorQueryHandler.cs
@@ -0,0 +1,63 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System.Collections.Generic;
+using System.Data.Entity;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Provides a base class for based query handlers which return a vector.
+///
+/// The type of the .
+/// The type of the .
+/// The type of the result.
+/// The type of the .
+/// The type of the DB entity.
+public abstract class DbContextVectorQueryHandler
+ : DbContextQueryHandler, TDbContext, TDbEntity>
+ where TQuery : IQuery>
+ where TEntity : class, IEntity
+ where TDbContext : DbContext
+ where TDbEntity : class
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ protected DbContextVectorQueryHandler(DbContextDataProvider provider)
+ : base(provider)
+ {
+ }
+
+ ///
+ /// Must be implemented to apply the query to the .
+ ///
+ /// The .
+ /// The to apply.
+ /// The with applied query.
+ protected abstract IQueryable ApplyQuery(IQueryable queryable, TQuery query);
+
+ ///
+ /// Must be implemented to project the result from to .
+ ///
+ /// The which must be projected.
+ /// The which is executed.
+ /// The projected .
+ protected abstract IQueryable ApplyProjection(IQueryable queryable, TQuery query);
+
+ ///
+ protected override async Task> QueryResultAsync(
+ IQueryable queryable,
+ TQuery query,
+ CancellationToken cancellationToken)
+ {
+ queryable = ApplyQuery(queryable, query);
+ IQueryable result = ApplyProjection(queryable, query);
+ return await result.ToArrayAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/DependencyInjection/EntityFrameworkDataProviderBuilder.cs b/src/AppCoreNet.Data.EntityFramework/DependencyInjection/EntityFrameworkDataProviderBuilder.cs
new file mode 100644
index 0000000..7341619
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DependencyInjection/EntityFrameworkDataProviderBuilder.cs
@@ -0,0 +1,142 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System;
+using System.ComponentModel;
+using System.Data.Entity;
+using AppCoreNet.Data;
+using AppCoreNet.Data.EntityFramework;
+using AppCoreNet.Diagnostics;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+// ReSharper disable once CheckNamespace
+namespace AppCoreNet.Extensions.DependencyInjection;
+
+///
+/// Represents the builder for Entity Framework data providers.
+///
+/// The type of the .
+public sealed class EntityFrameworkDataProviderBuilder
+ where TDbContext : DbContext
+{
+ ///
+ /// Gets the name of the provider.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public string Name { get; }
+
+ ///
+ /// Gets the .
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public IServiceCollection Services { get; }
+
+ ///
+ /// Gets the of the provider.
+ ///
+ public ServiceLifetime ProviderLifetime { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the data provider.
+ /// The .
+ /// The lifetime of the .
+ public EntityFrameworkDataProviderBuilder(
+ string name,
+ IServiceCollection services,
+ ServiceLifetime providerLifetime)
+ {
+ Ensure.Arg.NotNull(name);
+ Ensure.Arg.NotNull(services);
+
+ Name = name;
+ Services = services;
+ ProviderLifetime = providerLifetime;
+ }
+
+ ///
+ /// Adds the specified repository implementation.
+ ///
+ /// The type of the repository.
+ /// The .
+ public EntityFrameworkDataProviderBuilder AddRepository()
+ where TImplementation : class, IDbContextRepository
+ {
+ return AddRepository();
+ }
+
+ ///
+ /// Adds the specified repository implementation.
+ ///
+ /// The type of the repository service.
+ /// The type of the repository implementation.
+ /// The .
+ public EntityFrameworkDataProviderBuilder AddRepository()
+ where TService : class
+ where TImplementation : class, IDbContextRepository, TService
+ {
+ Services.TryAddEnumerable(
+ ServiceDescriptor.Describe(
+ typeof(TService),
+ new Func(sp =>
+ {
+ var provider =
+ (DbContextDataProvider)sp.GetRequiredService()
+ .Resolve(Name);
+
+ return ActivatorUtilities.CreateInstance(sp, provider);
+ }),
+ ProviderLifetime));
+
+ return this;
+ }
+
+ ///
+ /// Adds the specified query handler implementation.
+ ///
+ /// The type of the .
+ /// The .
+ public EntityFrameworkDataProviderBuilder AddQueryHandler()
+ where TQueryHandler : class, IDbContextQueryHandler
+ {
+ Type queryHandlerType = typeof(TQueryHandler);
+
+ Services.Configure(
+ Name,
+ o => o.QueryHandlerTypes.Add(queryHandlerType));
+
+ return this;
+ }
+
+ ///
+ /// Registers a which generates concurrency tokens.
+ ///
+ /// The type of the .
+ /// The .
+ public EntityFrameworkDataProviderBuilder AddTokenGenerator()
+ where T : class, ITokenGenerator
+ {
+ Services.Configure(
+ Name,
+ o => o.TokenGeneratorType = typeof(T));
+
+ return this;
+ }
+
+ ///
+ /// Registers a which maps entities to database entities.
+ ///
+ /// The type of the .
+ /// The .
+ public EntityFrameworkDataProviderBuilder AddEntityMapper()
+ where T : class, IEntityMapper
+ {
+ Services.Configure(
+ Name,
+ o => o.EntityMapperType = typeof(T));
+
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/DependencyInjection/EntityFrameworkDataProviderBuilderExtensions.cs b/src/AppCoreNet.Data.EntityFramework/DependencyInjection/EntityFrameworkDataProviderBuilderExtensions.cs
new file mode 100644
index 0000000..f6226de
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/DependencyInjection/EntityFrameworkDataProviderBuilderExtensions.cs
@@ -0,0 +1,74 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System.Data.Entity;
+using AppCoreNet.Data.EntityFramework;
+using AppCoreNet.Diagnostics;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+// ReSharper disable once CheckNamespace
+namespace AppCoreNet.Extensions.DependencyInjection;
+
+///
+/// Provides extension methods to register a .
+///
+public static class EntityFrameworkDataProviderBuilderExtensions
+{
+ ///
+ /// Registers a data provider in the .
+ ///
+ ///
+ /// Note that you have to register the on your own.
+ ///
+ /// The type of the .
+ /// The .
+ /// The name of the data provider.
+ /// The lifetime of the data provider.
+ /// The .
+ public static EntityFrameworkDataProviderBuilder AddEntityFramework(
+ this IDataProviderBuilder builder,
+ string name,
+ ServiceLifetime providerLifetime = ServiceLifetime.Scoped)
+ where TDbContext : DbContext
+ {
+ Ensure.Arg.NotNull(builder);
+ Ensure.Arg.NotNull(name);
+
+ builder.Services.AddOptions();
+ builder.Services.AddLogging();
+
+ builder.Services.TryAdd(ServiceDescriptor.Describe(typeof(TDbContext), typeof(TDbContext), providerLifetime));
+
+ builder.AddProvider>(
+ name,
+ providerLifetime,
+ static (sp, name) =>
+ {
+ DbContextDataProviderServices services =
+ DbContextDataProviderServices.Create(name, sp);
+
+ return new DbContextDataProvider(name, services);
+ });
+
+ return new EntityFrameworkDataProviderBuilder(name, builder.Services, providerLifetime);
+ }
+
+ ///
+ /// Registers a default data provider in the .
+ ///
+ ///
+ /// Note that you have to register the on your own.
+ ///
+ /// The type of the .
+ /// The .
+ /// The lifetime of the data provider.
+ /// The .
+ public static EntityFrameworkDataProviderBuilder AddEntityFramework(
+ this IDataProviderBuilder builder,
+ ServiceLifetime providerLifetime = ServiceLifetime.Scoped)
+ where TDbContext : DbContext
+ {
+ return builder.AddEntityFramework(string.Empty, providerLifetime);
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/IDbContextQueryHandler.cs b/src/AppCoreNet.Data.EntityFramework/IDbContextQueryHandler.cs
new file mode 100644
index 0000000..a0f7bb0
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/IDbContextQueryHandler.cs
@@ -0,0 +1,43 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System.Data.Entity;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Represents a based query handler.
+///
+/// The type of the .
+public interface IDbContextQueryHandler
+ where TDbContext : DbContext
+{
+}
+
+///
+/// Represents a handler for .
+///
+/// The type of the entity.
+/// The type of the result.
+/// The type of the .
+public interface IDbContextQueryHandler : IDbContextQueryHandler
+ where TEntity : class, IEntity
+ where TDbContext : DbContext
+{
+ ///
+ /// Gets a value indicating whether the query can be executed.
+ ///
+ /// The to test.
+ /// true if the query can be executed; otherwise, false.
+ bool CanExecute(IQuery query);
+
+ ///
+ /// Executes the given query.
+ ///
+ /// The to execute.
+ /// Token which can be used to cancel the process.
+ /// The result of the query.
+ Task ExecuteAsync(IQuery query, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/IDbContextRepository.cs b/src/AppCoreNet.Data.EntityFramework/IDbContextRepository.cs
new file mode 100644
index 0000000..0d342e4
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/IDbContextRepository.cs
@@ -0,0 +1,19 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System.Data.Entity;
+
+namespace AppCoreNet.Data.EntityFramework;
+
+///
+/// Represents a based repository.
+///
+/// The type of the .
+public interface IDbContextRepository
+ where TDbContext : DbContext
+{
+ ///
+ /// Gets the which owns the repository.
+ ///
+ DbContextDataProvider Provider { get; }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/Internal/DbModelProperties.cs b/src/AppCoreNet.Data.EntityFramework/Internal/DbModelProperties.cs
new file mode 100644
index 0000000..74ca71f
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/Internal/DbModelProperties.cs
@@ -0,0 +1,105 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Data.Entity;
+using System.Data.Entity.Core.Metadata.Edm;
+using System.Data.Entity.Core.Objects;
+using System.Data.Entity.Infrastructure;
+using System.Linq;
+
+// ReSharper disable once CheckNamespace
+namespace AppCoreNet.Data.EntityFramework;
+
+internal sealed class DbModelProperties
+{
+ private static readonly
+ ConcurrentDictionary<(Type DbContextType, Type DbEntityType, Type EntityType), DbModelProperties> _cache =
+ new();
+
+ public IReadOnlyList PrimaryKeyPropertyNames { get; private set; } = new List();
+
+ public bool HasConcurrencyToken { get; private set; }
+
+ public string? ConcurrencyTokenPropertyName { get; private set; }
+
+ private readonly Type _dbContextType;
+ private readonly Type _dbEntityType; // This is the type used in DbSet, potentially a proxy
+ private readonly Type _entityType; // This is the domain entity type IEntity
+
+ private DbModelProperties(DbContext dbContext, Type dbEntityType, Type entityType)
+ {
+ _dbContextType = dbContext.GetType();
+ _dbEntityType = dbEntityType;
+ _entityType = entityType;
+ Initialize(dbContext);
+ }
+
+ private void Initialize(DbContext dbContext)
+ {
+ ObjectContext objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;
+ MetadataWorkspace workspace = objectContext.MetadataWorkspace;
+
+ // Determine the non-proxy type for dbEntityType to ensure reliable metadata lookup
+ Type typeForMetadataLookup = _dbEntityType;
+ if (_dbEntityType.Namespace == "System.Data.Entity.DynamicProxies" && _dbEntityType.BaseType != null)
+ {
+ typeForMetadataLookup = _dbEntityType.BaseType;
+ }
+
+ // Get OSpace EntityType first, as dbEntityType is a CLR type
+ EntityType? ospaceEntityType = workspace
+ .GetItems(DataSpace.OSpace)
+ .FirstOrDefault(et => et.FullName == typeForMetadataLookup.FullName);
+
+ if (ospaceEntityType == null)
+ {
+ throw new InvalidOperationException($"Could not find OSpace EntityType for '{typeForMetadataLookup.FullName}' in MetadataWorkspace of DbContext '{_dbContextType.FullName}'.");
+ }
+
+ // Get corresponding CSpace EntityType
+ var cspaceEntityType = workspace.GetItem(ospaceEntityType.FullName, DataSpace.CSpace);
+ if (cspaceEntityType == null)
+ {
+ // This case should ideally not happen if OSpace type was found and metadata is consistent.
+ throw new InvalidOperationException($"Could not find CSpace EntityType corresponding to OSpace '{ospaceEntityType.FullName}' for DbContext '{_dbContextType.FullName}'.");
+ }
+
+ PrimaryKeyPropertyNames = cspaceEntityType.KeyProperties.Select(p => p.Name).ToList().AsReadOnly();
+
+ foreach (EdmProperty property in cspaceEntityType.Properties)
+ {
+ if (property.ConcurrencyMode == ConcurrencyMode.Fixed)
+ {
+ ConcurrencyTokenPropertyName = property.Name;
+ HasConcurrencyToken = true;
+ break;
+ }
+ }
+
+ // Validation logic
+ if (typeof(IHasChangeToken).IsAssignableFrom(_entityType)
+ || typeof(IHasChangeTokenEx).IsAssignableFrom(_entityType))
+ {
+ if (!HasConcurrencyToken)
+ {
+ throw new ArgumentException(
+ $"The entity type '{_entityType.FullName}' implements 'IHasChangeToken' or 'IHasChangeTokenEx' but no matching concurrency token property (ConcurrencyMode.Fixed) was found in the database model for DbEntityType '{_dbEntityType.FullName}' (resolved as '{typeForMetadataLookup.FullName}').");
+ }
+ }
+ else if (HasConcurrencyToken)
+ {
+ throw new ArgumentException(
+ $"The database model for DbEntityType '{_dbEntityType.FullName}' (resolved as '{typeForMetadataLookup.FullName}') contains concurrency token property '{ConcurrencyTokenPropertyName}' but the entity type '{_entityType.FullName}' does not implement 'IHasChangeToken' or 'IHasChangeTokenEx'.");
+ }
+ }
+
+ internal static DbModelProperties Get(DbContext dbContext, Type dbEntityType, Type entityType)
+ {
+ return _cache.GetOrAdd(
+ (dbContext.GetType(), DbEntityType: dbEntityType, EntityType: entityType),
+ key => new DbModelProperties(dbContext, key.DbEntityType, key.EntityType));
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/Internal/EntityModelProperties.cs b/src/AppCoreNet.Data.EntityFramework/Internal/EntityModelProperties.cs
new file mode 100644
index 0000000..3c3296e
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/Internal/EntityModelProperties.cs
@@ -0,0 +1,63 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+// ReSharper disable once CheckNamespace
+namespace AppCoreNet.Data.EntityFramework;
+
+internal sealed class EntityModelProperties
+ where TEntity : class, IEntity
+{
+ public Func GetIdValues { get; }
+
+ public IReadOnlyList IdPropertyNames { get; }
+
+ public EntityModelProperties()
+ {
+ Type entityType = typeof(TEntity);
+
+ Type entityIfaceType =
+ entityType.GetInterfaces()
+ .First(f => f.GetGenericTypeDefinition() == typeof(IEntity<>));
+
+ Type idType = entityIfaceType.GenericTypeArguments[0];
+ if (IsComplexIdType(idType))
+ {
+ PropertyInfo[] idProperties = idType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
+
+ IdPropertyNames = idProperties.Select(p => p.Name)
+ .ToList()
+ .AsReadOnly();
+
+ Func[] idPropertyGetters =
+ idProperties
+ .Select(p => new Func(o => p.GetValue(o)))
+ .ToArray();
+
+ GetIdValues = id =>
+ {
+ object?[] result = new object[idPropertyGetters.Length];
+ for (int i = 0; i < idPropertyGetters.Length; i++)
+ {
+ result[i] = idPropertyGetters[i](id);
+ }
+
+ return result;
+ };
+ }
+ else
+ {
+ IdPropertyNames = new List().AsReadOnly();
+ GetIdValues = id => new object?[] { id };
+ }
+ }
+
+ private static bool IsComplexIdType(Type idType)
+ {
+ return Type.GetTypeCode(idType) == TypeCode.Object && idType != typeof(Guid);
+ }
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/Internal/InternalsVisibleTo.cs b/src/AppCoreNet.Data.EntityFramework/Internal/InternalsVisibleTo.cs
new file mode 100644
index 0000000..1a54d94
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/Internal/InternalsVisibleTo.cs
@@ -0,0 +1,6 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("AppCoreNet.Data.EntityFramework.Tests")]
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/Internal/LogEventIds.cs b/src/AppCoreNet.Data.EntityFramework/Internal/LogEventIds.cs
new file mode 100644
index 0000000..9f1f20c
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/Internal/LogEventIds.cs
@@ -0,0 +1,41 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using Microsoft.Extensions.Logging;
+
+// ReSharper disable once CheckNamespace
+namespace AppCoreNet.Data.EntityFramework;
+
+internal static class LogEventIds
+{
+ // DbContextDataProvider
+ public static readonly EventId SavingChanges = new EventId(0, "saving_changes");
+
+ public static readonly EventId SavedChanges = new EventId(1, "saved_changes");
+
+ public static readonly EventId SaveChangesDeferred = new EventId(2, "saving_changes_delayed");
+
+ public static readonly EventId TransactionInit = new EventId(3, "transaction_init");
+
+ public static readonly EventId TransactionCommit = new EventId(4, "transaction_commit");
+
+ public static readonly EventId TransactionRollback = new EventId(5, "transaction_rollback");
+
+ public static readonly EventId TransactionDisposed = new EventId(6, "transaction_disposed");
+
+ // DbContextRepository
+ public static readonly EventId EntitySaving = new EventId(0, "entity_saving");
+
+ public static readonly EventId EntitySaved = new EventId(1, "entity_saved");
+
+ public static readonly EventId EntityDeleting = new EventId(2, "entity_deleting");
+
+ public static readonly EventId EntityDeleted = new EventId(3, "entity_deleted");
+
+ // DbContextQueryHandler
+ public static readonly EventId QueryExecuting = new EventId(0, "query_executing");
+
+ public static readonly EventId QueryExecuted = new EventId(1, "query_executed");
+
+ public static readonly EventId QueryFailed = new EventId(1, "query_failed");
+}
\ No newline at end of file
diff --git a/src/AppCoreNet.Data.EntityFramework/Internal/LoggerExtensions.cs b/src/AppCoreNet.Data.EntityFramework/Internal/LoggerExtensions.cs
new file mode 100644
index 0000000..d655c74
--- /dev/null
+++ b/src/AppCoreNet.Data.EntityFramework/Internal/LoggerExtensions.cs
@@ -0,0 +1,222 @@
+// Licensed under the MIT license.
+// Copyright (c) The AppCore .NET project.
+
+using System;
+using Microsoft.Extensions.Logging;
+
+// ReSharper disable once CheckNamespace
+namespace AppCoreNet.Data.EntityFramework;
+
+internal static class LoggerExtensions
+{
+ // DbContextDataProvider
+ private static readonly Action _savingChanges =
+ LoggerMessage.Define(
+ LogLevel.Trace,
+ LogEventIds.SavingChanges,
+ "Saving changes for context {dbContextType} ...");
+
+ private static readonly Action _savedChanges =
+ LoggerMessage.Define(
+ LogLevel.Debug,
+ LogEventIds.SavedChanges,
+ "Saved {entityCount} changes for context {dbContextType}.");
+
+ private static readonly Action _saveChangesDeferred =
+ LoggerMessage.Define(
+ LogLevel.Trace,
+ LogEventIds.SaveChangesDeferred,
+ "Deferred saving changes for context {dbContextType}.");
+
+ internal static void SavingChanges(this ILogger logger, Type dbContextType)
+ {
+ _savingChanges(logger, dbContextType, null);
+ }
+
+ internal static void SavedChanges(this ILogger logger, Type dbContextType, int entityCount)
+ {
+ _savedChanges(logger, entityCount, dbContextType, null);
+ }
+
+ internal static void SaveChangesDeferred(this ILogger logger, Type dbContextType)
+ {
+ _saveChangesDeferred(logger, dbContextType, null);
+ }
+
+ // DbContextTransactionManager
+ private static readonly Action _transactionInit =
+ LoggerMessage.Define(
+ LogLevel.Trace,
+ LogEventIds.TransactionInit,
+ "Initialized transaction {transactionId} for context {dbContextType}.");
+
+ private static readonly Action _transactionCommit =
+ LoggerMessage.Define(
+ LogLevel.Debug,
+ LogEventIds.TransactionCommit,
+ "Committed transaction {transactionId} for context {dbContextType}.");
+
+ private static readonly Action _transactionRollback =
+ LoggerMessage.Define(
+ LogLevel.Debug,
+ LogEventIds.TransactionRollback,
+ "Rolled back transaction {transactionId} for context {dbContextType}.");
+
+ private static readonly Action _transactionDisposed =
+ LoggerMessage.Define(
+ LogLevel.Trace,
+ LogEventIds.TransactionDisposed,
+ "Disposed transaction {transactionId} for context {dbContextType}.");
+
+ internal static void TransactionInit(this ILogger logger, Type dbContextType, string transactionId)
+ {
+ _transactionInit(logger, transactionId, dbContextType, null);
+ }
+
+ internal static void TransactionCommit(this ILogger logger, Type dbContextType, string transactionId)
+ {
+ _transactionCommit(logger, transactionId, dbContextType, null);
+ }
+
+ internal static void TransactionRollback(this ILogger logger, Type dbContextType, string transactionId)
+ {
+ _transactionRollback(logger, transactionId, dbContextType, null);
+ }
+
+ internal static void TransactionDisposed(this ILogger logger, Type dbContextType, string transactionId)
+ {
+ _transactionDisposed(logger, transactionId, dbContextType, null);
+ }
+
+ // DbContextRepository
+ private static readonly Action _entitySaving =
+ LoggerMessage.Define(
+ LogLevel.Debug,
+ LogEventIds.EntitySaving,
+ "Saving entity {entityType} with id {entityId} ...");
+
+ private static readonly Action _concurrentEntitySaving =
+ LoggerMessage.Define(
+ LogLevel.Debug,
+ LogEventIds.EntitySaving,
+ "Saving entity {entityType} with id {entityId} and token {entityConcurrencyToken} ...");
+
+ private static readonly Action _entitySaved =
+ LoggerMessage.Define(
+ LogLevel.Information,
+ LogEventIds.EntitySaved,
+ "Entity {entityType} with id {entityId} saved.");
+
+ private static readonly Action _concurrentEntitySaved =
+ LoggerMessage.Define(
+ LogLevel.Information,
+ LogEventIds.EntitySaved,
+ "Entity {entityType} with id {entityId} and token {entityConcurrencyToken} saved.");
+
+ private static readonly Action _entityDeleting =
+ LoggerMessage.Define(
+ LogLevel.Debug,
+ LogEventIds.EntityDeleting,
+ "Deleting entity {entityType} with id {entityId} ...");
+
+ private static readonly Action _concurrentEntityDeleting =
+ LoggerMessage.Define(
+ LogLevel.Debug,
+ LogEventIds.EntityDeleting,
+ "Deleting entity {entityType} with id {entityId} and token {entityConcurrencyToken} ...");
+
+ private static readonly Action _entityDeleted =
+ LoggerMessage.Define(
+ LogLevel.Information,
+ LogEventIds.EntityDeleted,
+ "Entity {entityType} with id {entityId} deleted.");
+
+ public static void EntitySaving(this ILogger logger, IEntity entity)
+ {
+ if (entity is IHasChangeToken concurrentEntity)
+ {
+ _concurrentEntitySaving(
+ logger,
+ entity.GetType(),
+ entity.Id,
+ concurrentEntity.ChangeToken,
+ null);
+ }
+ else
+ {
+ _entitySaving(logger, entity.GetType(), entity.Id, null);
+ }
+ }
+
+ public static void EntitySaved(this ILogger logger, IEntity