-
Notifications
You must be signed in to change notification settings - Fork 245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add two methods for consuming repositories in scenarios where repositories could be longer lived (e.g. Blazor component Injections) #289
Merged
Merged
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
32dc155
Add two methods for consuming repositories in scenarios where reposit…
jasonsummers cb4c9d0
Add integration tests to validate behaviour of ContextFactoryReposito…
jasonsummers 78a2361
Merge branch 'main' into manage-context-lifetime
jasonsummers 79617dd
Add more integration tests for ContextFactoryRepositoryBaseOfT and te…
jasonsummers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(); | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
....EntityFrameworkCore.UnitTests/Ardalis.Specification.EntityFrameworkCore.UnitTests.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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
|
||
<IsPackable>false</IsPackable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> | ||
<PackageReference Include="Moq" Version="4.18.2" /> | ||
<PackageReference Include="xunit" Version="2.4.1" /> | ||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
<PrivateAssets>all</PrivateAssets> | ||
</PackageReference> | ||
<PackageReference Include="coverlet.collector" Version="3.1.2"> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
<PrivateAssets>all</PrivateAssets> | ||
</PackageReference> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\..\sample\Ardalis.SampleApp.Infrastructure\Ardalis.SampleApp.Infrastructure.csproj" /> | ||
<ProjectReference Include="..\..\src\Ardalis.Specification.EntityFrameworkCore\Ardalis.Specification.EntityFrameworkCore.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
27 changes: 27 additions & 0 deletions
27
...ore/tests/Ardalis.Specification.EntityFrameworkCore.UnitTests/EFRepositoryFactoryTests.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,27 @@ | ||
using Ardalis.SampleApp.Core.Entities.CustomerAggregate; | ||
using Ardalis.SampleApp.Core.Interfaces; | ||
using Ardalis.SampleApp.Infrastructure.Data; | ||
using Ardalis.SampleApp.Infrastructure.DataAccess; | ||
using Microsoft.EntityFrameworkCore; | ||
using Moq; | ||
using Xunit; | ||
|
||
namespace Ardalis.Specification.EntityFrameworkCore.UnitTests; | ||
|
||
public class UnitTest1 | ||
{ | ||
[Fact] | ||
public void CorrectlyInstantiatesRepository() | ||
{ | ||
var mockContextFactory = new Mock<IDbContextFactory<SampleDbContext>>(); | ||
mockContextFactory.Setup(x => x.CreateDbContext()) | ||
.Returns(() => new SampleDbContext(new DbContextOptions<SampleDbContext>())); | ||
|
||
var repositoryFactory = | ||
new EFRepositoryFactory<IRepository<Customer>, MyRepository<Customer>, SampleDbContext>(mockContextFactory | ||
.Object); | ||
|
||
var repository = repositoryFactory.CreateRepository(); | ||
Assert.IsType<MyRepository<Customer>>(repository); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is using
using
won't it make change tracking and later updates of the fetched entity impossible? The dbContext that originally is tracking the entity will be disposed by the end of this method, and so when a savechanges is called there will be an error saying "entity is already tracked by another dbcontext" or something equivalent, right? If not, can you write another few tests demonstrating that your solution works for fetch-change-save operations?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies Steve, I didn't give that fact much attention in my initial request.
Yes, I would expect such an error with this approach. I'll add a virtual protected method which invokes the
TrackGraph
method inside theusing
to solve this. The original intention was to allow for maximum flexibility but on reflection a default approach to managing the change tracker is definitely appropriate.I just need to work through getting the error we're expecting to present itself in tests to ensure it's being handled appropriately.
I'll post another update here when the above is completed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jasonsummers, was this done? If so, where? Either way, can you explain this for me please?
Thanks,
Matt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mwasson74, this wasn't completed in the end, mainly because the only solutions I could come up with added too much opinion into the library.
Essentially, due to the prolonged lifecycle of Blazor (and WPF/UWP/MAUI etc) apps, the new ContextFactoryRepositoryBaseOfT class instantiates a new instance of the DBContext every time a method is invoked. This means that all of the Entity Framework Change Tracking goodness is lost.
From a DDD perspective, this actually makes sense, whether or not an Entity has changed is a subject for the Entity to manage itself and not be reliant on a 3rd party to deduce.
What this means is that you need to implement change tracking manually within your solution. I think the only method you'll have to overload is the UpdateAsync method since that's the only one which really needs to know what's changed.
Sorry this is so vague, it's been a while since I've looked at this. Reply back here if you need more info. I'll try and carve out some time to write a proper doc for this as well.