Skip to content

Commit

Permalink
Add two methods for consuming repositories in scenarios where reposit…
Browse files Browse the repository at this point in the history
…ories could be longer lived (e.g. Blazor component Injections) (#289)

* Add two methods for consuming repositories in scenarios where repositories could be longer lived (e.g. Blazor component Injections)

- BREAKING CHANGE - requires support for netstandard2.0 to be dropped from Ardalis.Specification.EntityFrameworkCore.csproj in order to make use of IDbContextFactory
- Add IRepositoryFactory interface and EFRepositoryFactory concrete implementation to encapsulate the 'Unit of Work' principle at the repository level, consuming DbContextFactories from DI containers such as those added using the .AddDbContextFactory method, following blazor best practices for managing DbContext lifetimes
- Add ContextFactoryRepositoryBaseOfT.cs abstract implementation of IRepositoryBase<T> which again consumes DbContextFactories from DI containers such as those added using the .AddDbContextFactory method but creates a new instance of the DbContext for every method call in the repository. This breaks Entity Framework change tracking so Update and Delete methods will have to be overloaded in concrete implementations using the TrackChanges method on the context.

* Add integration tests to validate behaviour of ContextFactoryRepositoryOfT.cs

* Add more integration tests for ContextFactoryRepositoryBaseOfT and tests for EFRepositoryFactory

---------

Co-authored-by: Jason Summers <[email protected]>
  • Loading branch information
jasonsummers and jasonsummers authored Apr 11, 2023
1 parent 39e082b commit da63643
Show file tree
Hide file tree
Showing 12 changed files with 620 additions and 1 deletion.
7 changes: 7 additions & 0 deletions Ardalis.Specification.sln
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Specification.EntityFramewo
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Specification.EntityFramework6", "Specification.EntityFramework6", "{327AEBD6-C8A6-4851-BA42-632F8014CFC5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ardalis.Specification.EntityFrameworkCore.UnitTests", "Specification.EntityFrameworkCore\tests\Ardalis.Specification.EntityFrameworkCore.UnitTests\Ardalis.Specification.EntityFrameworkCore.UnitTests.csproj", "{53E4FFB4-CAC0-482D-B714-FA657C3244C9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -86,6 +88,10 @@ Global
{4BEB4DC4-DE33-4DF1-8A2F-CE76C1D72A4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4BEB4DC4-DE33-4DF1-8A2F-CE76C1D72A4A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4BEB4DC4-DE33-4DF1-8A2F-CE76C1D72A4A}.Release|Any CPU.Build.0 = Release|Any CPU
{53E4FFB4-CAC0-482D-B714-FA657C3244C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{53E4FFB4-CAC0-482D-B714-FA657C3244C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53E4FFB4-CAC0-482D-B714-FA657C3244C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53E4FFB4-CAC0-482D-B714-FA657C3244C9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -100,6 +106,7 @@ Global
{5AFD1454-E625-451D-A615-CEB7BB09AA65} = {B19F2F64-4B22-48C2-B2F8-7672F84F758D}
{37EC09C7-702D-4539-B98D-F67B15E1E6CE} = {327AEBD6-C8A6-4851-BA42-632F8014CFC5}
{4BEB4DC4-DE33-4DF1-8A2F-CE76C1D72A4A} = {327AEBD6-C8A6-4851-BA42-632F8014CFC5}
{53E4FFB4-CAC0-482D-B714-FA657C3244C9} = {B19F2F64-4B22-48C2-B2F8-7672F84F758D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C153A625-42F7-49A7-B99A-6A78B4B866B2}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.1;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
<PackageId>Ardalis.Specification.EntityFrameworkCore</PackageId>
<Title>Ardalis.Specification.EntityFrameworkCore</Title>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace Ardalis.Specification.EntityFrameworkCore
{
public abstract class ContextFactoryRepositoryBaseOfT<TEntity, TContext> : IRepositoryBase<TEntity>
where TEntity : class
where TContext : DbContext
{
private IDbContextFactory<TContext> dbContextFactory;
private ISpecificationEvaluator specificationEvaluator;

public ContextFactoryRepositoryBaseOfT(IDbContextFactory<TContext> dbContextFactory)
: this(dbContextFactory, SpecificationEvaluator.Default)
{
}

public ContextFactoryRepositoryBaseOfT(IDbContextFactory<TContext> dbContextFactory,
ISpecificationEvaluator specificationEvaluator)
{
this.dbContextFactory = dbContextFactory;
this.specificationEvaluator = specificationEvaluator;
}

/// <inheritdoc/>
public async Task<TEntity?> GetByIdAsync<TId>(TId id, CancellationToken cancellationToken = default) where TId : notnull
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await dbContext.Set<TEntity>().FindAsync(new object[] { id }, cancellationToken: cancellationToken);
}

/// <inheritdoc/>
public async Task<TEntity?> GetBySpecAsync(ISpecification<TEntity> specification, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<TResult?> GetBySpecAsync<TResult>(ISpecification<TEntity, TResult> specification, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<TEntity?> FirstOrDefaultAsync(ISpecification<TEntity> specification, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<TResult?> FirstOrDefaultAsync<TResult>(ISpecification<TEntity, TResult> specification, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<TEntity?> SingleOrDefaultAsync(ISingleResultSpecification<TEntity> specification, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<TResult?> SingleOrDefaultAsync<TResult>(ISingleResultSpecification<TEntity, TResult> specification,
CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<List<TEntity>> ListAsync(CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await dbContext.Set<TEntity>().ToListAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<List<TEntity>> ListAsync(ISpecification<TEntity> specification, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
var queryResult = await ApplySpecification(specification, dbContext).ToListAsync(cancellationToken);

return specification.PostProcessingAction == null ? queryResult : specification.PostProcessingAction(queryResult).ToList();
}

/// <inheritdoc/>
public async Task<List<TResult>> ListAsync<TResult>(ISpecification<TEntity, TResult> specification, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
var queryResult = await ApplySpecification(specification, dbContext).ToListAsync(cancellationToken);

return specification.PostProcessingAction == null ? queryResult : specification.PostProcessingAction(queryResult).ToList();
}

/// <inheritdoc/>
public async Task<int> CountAsync(ISpecification<TEntity> specification, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await ApplySpecification(specification, dbContext, true).CountAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<int> CountAsync(CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await dbContext.Set<TEntity>().CountAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<bool> AnyAsync(ISpecification<TEntity> specification, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await ApplySpecification(specification, dbContext, true).AnyAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<bool> AnyAsync(CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
return await dbContext.Set<TEntity>().AnyAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
dbContext.Set<TEntity>().Add(entity);

await SaveChangesAsync(dbContext, cancellationToken);

return entity;
}

/// <inheritdoc/>
public async Task<IEnumerable<TEntity>> AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
dbContext.Set<TEntity>().AddRange(entities);

await SaveChangesAsync(dbContext, cancellationToken);

return entities;
}

/// <inheritdoc/>
public async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
dbContext.Set<TEntity>().Update(entity);

await SaveChangesAsync(dbContext, cancellationToken);
}

/// <inheritdoc/>
public async Task UpdateRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
dbContext.Set<TEntity>().UpdateRange(entities);

await SaveChangesAsync(dbContext, cancellationToken);
}

/// <inheritdoc/>
public async Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
dbContext.Set<TEntity>().Remove(entity);

await SaveChangesAsync(dbContext, cancellationToken);
}

/// <inheritdoc/>
public async Task DeleteRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default)
{
await using var dbContext = this.dbContextFactory.CreateDbContext();
dbContext.Set<TEntity>().RemoveRange(entities);

await SaveChangesAsync(dbContext, cancellationToken);
}

/// <inheritdoc/>
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
throw new InvalidOperationException();
}

public async Task<int> SaveChangesAsync(TContext dbContext, CancellationToken cancellationToken = default)
{
return await dbContext.SaveChangesAsync(cancellationToken);
}

/// <summary>
/// Filters the entities of <typeparamref name="TEntity"/>, to those that match the encapsulated query logic of the
/// <paramref name="specification"/>.
/// </summary>
/// <param name="specification">The encapsulated query logic.</param>
/// <returns>The filtered entities as an <see cref="IQueryable{T}"/>.</returns>
protected virtual IQueryable<TEntity> ApplySpecification(ISpecification<TEntity> specification, TContext dbContext, bool evaluateCriteriaOnly = false)
{
return specificationEvaluator.GetQuery(dbContext.Set<TEntity>().AsQueryable(), specification, evaluateCriteriaOnly);
}

/// <summary>
/// Filters all entities of <typeparamref name="TEntity" />, that matches the encapsulated query logic of the
/// <paramref name="specification"/>, from the database.
/// <para>
/// Projects each entity into a new form, being <typeparamref name="TResult" />.
/// </para>
/// </summary>
/// <typeparam name="TResult">The type of the value returned by the projection.</typeparam>
/// <param name="specification">The encapsulated query logic.</param>
/// <returns>The filtered projected entities as an <see cref="IQueryable{T}"/>.</returns>
protected virtual IQueryable<TResult> ApplySpecification<TResult>(ISpecification<TEntity, TResult> specification, TContext dbContext)
{
return specificationEvaluator.GetQuery(dbContext.Set<TEntity>().AsQueryable(), specification);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using Microsoft.EntityFrameworkCore;

namespace Ardalis.Specification.EntityFrameworkCore
{
/// <summary>
///
/// </summary>
/// <typeparam name="TRepository">The Interface of the repository created by this Factory</typeparam>
/// <typeparam name="TConcreteRepository">
/// The Concrete implementation of the repository interface to create
/// </typeparam>
/// <typeparam name="TContext">The DbContext derived class to support the concrete repository</typeparam>
public class EFRepositoryFactory<TRepository, TConcreteRepository, TContext> : IRepositoryFactory<TRepository>
where TConcreteRepository : TRepository
where TContext : DbContext
{
private IDbContextFactory<TContext> dbContextFactory;

/// <summary>
/// Initialises a new instance of the EFRepositoryFactory
/// </summary>
/// <param name="dbContextFactory">The IDbContextFactory to use to generate the TContext</param>
public EFRepositoryFactory(IDbContextFactory<TContext> dbContextFactory)
{
this.dbContextFactory = dbContextFactory;
}

/// <inheritdoc />
public TRepository CreateRepository()
{
var args = new object[] { dbContextFactory.CreateDbContext() };
return (TRepository)Activator.CreateInstance(typeof(TConcreteRepository), args);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Ardalis.Specification.EntityFrameworkCore
{
/// <summary>
/// Generates new instances of <typeparamref name="TRepository"/> to encapsulate the 'Unit of Work' pattern
/// in scenarios where injected types may be long-lived (e.g. Blazor)
/// </summary>
/// <typeparam name="TRepository">
/// The Interface of the Repository to be generated.
/// </typeparam>
public interface IRepositoryFactory<TRepository>
{
/// <summary>
/// Generates a new repository instance
/// </summary>
/// <returns>The generated repository instance</returns>
public TRepository CreateRepository();
}
}
Loading

0 comments on commit da63643

Please sign in to comment.