Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions AppCoreNet.Data.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

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

<PropertyGroup>
<TargetFrameworks>net462;netstandard2.1;net8.0</TargetFrameworks>
<Description>Adds EntityFramework (EF6) support to AppCore .NET persistence.</Description>
<PackageTags>$(PackageTags);EntityFramework;EF6</PackageTags>
<RootNamespace>AppCoreNet.Data.EntityFramework</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="EntityFramework" VersionOverride="6.5.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AppCoreNet.Data\AppCoreNet.Data.csproj" />
</ItemGroup>

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

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Description>Adds EntityFramework Core support to AppCore .NET persistence.</Description>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" VersionOverride="8.0.16" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AppCoreNet.Data\AppCoreNet.Data.csproj" />
</ItemGroup>

</Project>
102 changes: 102 additions & 0 deletions src/AppCoreNet.Data.EntityFramework/DbContextDataProvider.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a Entity Framework data provider.
/// </summary>
/// <typeparam name="TDbContext">The type of the <see cref="DbContext"/>.</typeparam>
public sealed class DbContextDataProvider<TDbContext> : IDataProvider
where TDbContext : DbContext
{
private readonly string _name;
private readonly DbContextDataProviderServices<TDbContext> _services;

/// <inheritdoc />
public string Name => _name;

/// <summary>
/// Gets the <see cref="DbContext"/> of the data provider.
/// </summary>
/// <value>The <see cref="DbContext"/>.</value>
public TDbContext DbContext => _services.DbContext;

/// <summary>
/// Gets the <see cref="IEntityMapper"/> of the data provider.
/// </summary>
public IEntityMapper EntityMapper => _services.EntityMapper;

/// <summary>
/// Gets the <see cref="ITokenGenerator"/> of the data provider.
/// </summary>
public ITokenGenerator TokenGenerator => _services.TokenGenerator;

/// <summary>
/// Gets the <see cref="DbContextQueryHandlerFactory{TDbContext}"/> of the data provider.
/// </summary>
public DbContextQueryHandlerFactory<TDbContext> QueryHandlerFactory => _services.QueryHandlerFactory;

/// <summary>
/// Gets the <see cref="DbContextTransactionManager{TDbContext}"/>.
/// </summary>
public DbContextTransactionManager<TDbContext> TransactionManager => _services.TransactionManager;

ITransactionManager IDataProvider.TransactionManager => TransactionManager;

internal DataProviderLogger<DbContextDataProvider<TDbContext>> Logger => _services.Logger;

/// <summary>
/// Initializes a new instance of the <see cref="DbContextDataProvider{TDbContext}"/> class.
/// </summary>
/// <param name="name">The name of the data provider.</param>
/// <param name="services">The <see cref="DbContextDataProviderServices{TDbContext}"/>.</param>
public DbContextDataProvider(
string name,
DbContextDataProviderServices<TDbContext> services)
{
Ensure.Arg.NotNull(name);
Ensure.Arg.NotNull(services);

_name = name;
_services = services;
}

/// <summary>
/// Saves changes made to the <see cref="DbContext"/>.
/// </summary>
/// <param name="cancellationToken">Optional <see cref="CancellationToken"/>.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
try
{
// EF6 SaveChangesAsync returns Task<int>
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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Type> QueryHandlerTypes { get; } = new HashSet<Type>();
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides services for <see cref="DbContext"/> based data provider.
/// </summary>
/// <typeparam name="TDbContext">The type of the <see cref="DbContext"/>.</typeparam>
public sealed class DbContextDataProviderServices<TDbContext>
where TDbContext : DbContext
{
/// <summary>
/// Gets the <see cref="DbContext"/>.
/// </summary>
public TDbContext DbContext { get; }

/// <summary>
/// Gets the <see cref="IEntityMapper"/>.
/// </summary>
public IEntityMapper EntityMapper { get; }

/// <summary>
/// Gets the <see cref="ITokenGenerator"/>.
/// </summary>
public ITokenGenerator TokenGenerator { get; }

/// <summary>
/// Gets the <see cref="DbContextQueryHandlerFactory{TDbContext}"/>.
/// </summary>
public DbContextQueryHandlerFactory<TDbContext> QueryHandlerFactory { get; }

/// <summary>
/// Gets the <see cref="DbContextTransactionManager{TDbContext}"/>.
/// </summary>
public DbContextTransactionManager<TDbContext> TransactionManager { get; }

/// <summary>
/// Gets the <see cref="ILogger"/>.
/// </summary>
public DataProviderLogger<DbContextDataProvider<TDbContext>> Logger { get; }

internal DbContextDataProviderServices(
TDbContext dbContext,
IEntityMapper entityMapper,
ITokenGenerator tokenGenerator,
DbContextQueryHandlerFactory<TDbContext> queryHandlerFactory,
DbContextTransactionManager<TDbContext> transactionManager,
DataProviderLogger<DbContextDataProvider<TDbContext>> logger)
{
DbContext = dbContext;
EntityMapper = entityMapper;
TokenGenerator = tokenGenerator;
QueryHandlerFactory = queryHandlerFactory;
TransactionManager = transactionManager;
Logger = logger;
}

private static T GetOrCreateInstance<T>(IServiceProvider serviceProvider, Type? type)
where T : notnull
{
return type != null
? (T)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, type)
: serviceProvider.GetRequiredService<T>();
}

internal static DbContextDataProviderServices<TDbContext> Create(string name, IServiceProvider serviceProvider)
{
var optionsMonitor = serviceProvider.GetRequiredService<IOptionsMonitor<DbContextDataProviderOptions>>();
DbContextDataProviderOptions options = optionsMonitor.Get(name);

var entityMapper = GetOrCreateInstance<IEntityMapper>(serviceProvider, options.EntityMapperType);
var tokenGenerator = GetOrCreateInstance<ITokenGenerator>(serviceProvider, options.TokenGeneratorType);
var logger = serviceProvider.GetRequiredService<DataProviderLogger<DbContextDataProvider<TDbContext>>>();

var dbContext = serviceProvider.GetRequiredService<TDbContext>();
var queryHandlerFactory = new DbContextQueryHandlerFactory<TDbContext>(serviceProvider, options.QueryHandlerTypes);
var transactionManager = new DbContextTransactionManager<TDbContext>(dbContext, logger);

return new DbContextDataProviderServices<TDbContext>(
dbContext,
entityMapper,
tokenGenerator,
queryHandlerFactory,
transactionManager,
logger);
}
}
86 changes: 86 additions & 0 deletions src/AppCoreNet.Data.EntityFramework/DbContextPagedQueryHandler.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides a base class for <see cref="DbContext"/> based query handlers which return a page of the result set.
/// </summary>
/// <typeparam name="TQuery">The type of the <see cref="IQuery{TEntity,TResult}"/>.</typeparam>
/// <typeparam name="TEntity">The type of the <see cref="IEntity"/>.</typeparam>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <typeparam name="TDbContext">The type of the <see cref="DbContext"/>.</typeparam>
/// <typeparam name="TDbEntity">The type of the DB entity.</typeparam>
public abstract class DbContextPagedQueryHandler<TQuery, TEntity, TResult, TDbContext, TDbEntity>
: DbContextQueryHandler<TQuery, TEntity, IPagedResult<TResult>, TDbContext, TDbEntity>
where TQuery : IPagedQuery<TEntity, TResult>
where TEntity : class, IEntity
where TDbContext : DbContext
where TDbEntity : class
{
/// <summary>
/// Initializes a new instance of the <see cref="DbContextPagedQueryHandler{TQuery,TEntity,TResult,TDbContext,TDbEntity}"/> class.
/// </summary>
/// <param name="provider">The <see cref="DbContextDataProvider{TDbContext}"/>.</param>
protected DbContextPagedQueryHandler(DbContextDataProvider<TDbContext> provider)
: base(provider)
{
}

/// <summary>
/// Must be implemented to apply the query to the <see cref="IQueryable{T}"/>.
/// </summary>
/// <param name="queryable">The <see cref="IQueryable{T}"/>.</param>
/// <param name="query">The <see cref="IQuery{TEntity,TResult}"/> to apply.</param>
/// <returns>The <see cref="IQueryable{T}"/> with the query applied.</returns>
protected abstract IQueryable<TDbEntity> ApplyQuery(IQueryable<TDbEntity> queryable, TQuery query);

/// <summary>
/// Must be implemented to project the result from <typeparamref name="TEntity"/> to <typeparamref name="TResult"/>.
/// </summary>
/// <param name="queryable">The <see cref="IQueryable{T}"/> which must be projected.</param>
/// <param name="query">The <see cref="IQuery{TEntity,TResult}"/> which is executed.</param>
/// <returns>The projected <see cref="IQueryable{T}"/>.</returns>
protected abstract IQueryable<TResult> ApplyProjection(IQueryable<TDbEntity> queryable, TQuery query);

/// <summary>
/// Can be overriden to customize the paging.
/// </summary>
/// <param name="queryable">The <see cref="IQueryable{T}"/> which must be paged.</param>
/// <param name="query">The <see cref="IQuery{TEntity,TResult}"/> which is executed.</param>
/// <returns>The projected <see cref="IQueryable{T}"/>.</returns>
protected virtual IQueryable<TDbEntity> ApplyPaging(IQueryable<TDbEntity> queryable, TQuery query)
{
if (query.Offset > 0)
queryable = queryable.Skip((int)query.Offset);

return queryable.Take(query.Limit);
}

/// <inheritdoc />
protected override async Task<IPagedResult<TResult>> QueryResultAsync(
IQueryable<TDbEntity> 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<TResult> projectedQueryable = ApplyProjection(queryable, query);

TResult[] result = await projectedQueryable.ToArrayAsync(cancellationToken)
.ConfigureAwait(false);

return new PagedResult<TResult>(result, totalCount);
}
}
Loading
Loading