-
Notifications
You must be signed in to change notification settings - Fork 245
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add two methods for consuming repositories in scenarios where reposit…
…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
1 parent
39e082b
commit da63643
Showing
12 changed files
with
620 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...rdalis.Specification.EntityFrameworkCore/Ardalis.Specification.EntityFrameworkCore.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
227 changes: 227 additions & 0 deletions
227
...workCore/src/Ardalis.Specification.EntityFrameworkCore/ContextFactoryRepositoryBaseOfT.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
....EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/EFRepositoryFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
...n.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/IRepositoryFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
Oops, something went wrong.