From d132a6cafcf1cbb75c1d356701685e5370cb8946 Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 19:00:13 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20extract=20?= =?UTF-8?q?a=20shared=20EntityFrameworkUnitOfWork=20base=20for=20the=20doc?= =?UTF-8?q?ument=20providers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MongoDb and (incoming) CosmosDb unit of works were near-identical copies of the same ~200 lines of store-agnostic IQueryableUnitOfWork boilerplate (Commit, GetRepository, ApplyCurrentValues, LoadCollection, Dispose, ...). Extracts that into an abstract EntityFrameworkUnitOfWork in the base package, with a single abstract CreateSetCore() each provider overrides to build its own Set. MongoDb's unit of work drops to ~15 lines. The relational providers keep RelationalUnitOfWork (it also is a raw-SQL executor, a separate concern). Behaviour is unchanged; base/Relational/SqlServer/MongoDb build and all 30 tests pass. --- .../Repository/UnitOfWork.cs | 240 +---------------- .../Repository/EntityFrameworkUnitOfWork.cs | 249 ++++++++++++++++++ 2 files changed, 257 insertions(+), 232 deletions(-) create mode 100644 src/eQuantic.Core.Data.EntityFramework/Repository/EntityFrameworkUnitOfWork.cs diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs index 8e38967..9da4ad4 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs @@ -1,243 +1,19 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; +using eQuantic.Core.Data.EntityFramework.Repository; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Options; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace eQuantic.Core.Data.EntityFramework.MongoDb.Repository; -public abstract class UnitOfWork : IQueryableUnitOfWork +/// +/// The MongoDB unit of work. All of the store-agnostic behaviour lives in +/// ; this only supplies the MongoDB . +/// +public abstract class UnitOfWork(IServiceProvider serviceProvider, DbContext context) + : EntityFrameworkUnitOfWork(serviceProvider, context) { - protected readonly IServiceProvider ServiceProvider; - /// - /// The context - /// - protected readonly DbContext Context; - - /// - /// The disposed - /// - protected bool Disposed; - - /// - /// Initializes a new instance of the class - /// - /// - /// The context - protected UnitOfWork(IServiceProvider serviceProvider, DbContext context) - { - ServiceProvider = serviceProvider; - Context = context; - } - - public int Commit() - { - return Context.SaveChanges(); - } - - public int CommitAndRefreshChanges() - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = Context.SaveChanges(); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues()!)); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = await Context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues()!)); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAsync(CancellationToken cancellationToken = default) - { - return await Context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - - public int Commit(Action options) - { - return Commit(); - } - - public int CommitAndRefreshChanges(Action options) - { - return CommitAndRefreshChanges(); - } - - public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAndRefreshChangesAsync(cancellationToken); - } - - public Task CommitAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAsync(cancellationToken); - } - - public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity - { - ((Set)InternalCreateSet()).ApplyCurrentValues(original, current); - } - - public void Attach(TEntity item) where TEntity : class, IEntity - { - ((Set)InternalCreateSet()).Attach(item); - } - - public void LoadCollection(TEntity item, - Expression>> navigationProperty, - Expression>? filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - Context.Entry(item).Collection(navigationProperty).Query().Where(filter).Load(); - } - else - { - Context.Entry(item).Collection(navigationProperty).Load(); - } - } - - public async Task LoadCollectionAsync(TEntity item, - Expression>> navigationProperty, - Expression>? filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - await Context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); - } - else - { - await Context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); - } - } - - public void Reload(TEntity item) where TEntity : class - { - var entry = Context.Entry(item); - entry.CurrentValues.SetValues(entry.OriginalValues); - entry.Reload(); - } - - public void RollbackChanges() - { - // set all entities in change tracker - // as 'unchanged state' - Context?.ChangeTracker.Entries() - .ToList() - .ForEach(entry => entry.State = EntityState.Unchanged); - } - - public void SetModified(TEntity item) where TEntity : class - { - //this operation also attach item in object state manager - Context.Entry(item).State = EntityState.Modified; - } - - public virtual SaveOptions GetSaveOptions() - { - return new SaveOptions(); - } - - public virtual IRepository GetRepository() - where TEntity : class, IEntity - { - return ServiceProvider.GetRequiredService>(); - } - - public IAsyncRepository GetAsyncRepository() - where TEntity : class, IEntity - { - return ServiceProvider.GetRequiredService>(); - } - - public Data.Repository.ISet CreateSet() where TEntity : class, IEntity => InternalCreateSet(); - - public IQueryableRepository GetQueryableRepository() - where TEntity : class, IEntity - { - return ServiceProvider.GetRequiredService>(); - } - - public IAsyncQueryableRepository GetAsyncQueryableRepository() - where TEntity : class, IEntity - { - return ServiceProvider.GetRequiredService>(); - } - - internal DbContext GetDbContext() => Context; - - internal Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity => + protected override Data.Repository.ISet CreateSetCore() => new Set(ServiceProvider, Context); - - /// - /// Disposes this instance - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes the disposing - /// - /// The disposing - protected virtual void Dispose(bool disposing) - { - if (Disposed) - { - return; - } - - if (disposing) - { - Context?.Dispose(); - } - - Disposed = true; - } } public abstract class UnitOfWork(IServiceProvider serviceProvider, TDbContext context) diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/EntityFrameworkUnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/EntityFrameworkUnitOfWork.cs new file mode 100644 index 0000000..eaf8ecf --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/EntityFrameworkUnitOfWork.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using eQuantic.Core.Data.Repository; +using eQuantic.Core.Data.Repository.Options; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace eQuantic.Core.Data.EntityFramework.Repository; + +/// +/// The shared, provider-agnostic implementation for the document +/// (non-relational) EF Core providers. It carries every unit-of-work member that does not depend on the +/// store; each provider supplies only its own entity set by overriding . +/// The relational providers keep their own unit of work (it also is a raw-SQL executor). +/// +public abstract class EntityFrameworkUnitOfWork : IQueryableUnitOfWork +{ + /// The service provider used to resolve repositories. + protected readonly IServiceProvider ServiceProvider; + + /// The underlying . + protected readonly DbContext Context; + + /// Whether this instance has been disposed. + protected bool Disposed; + + /// Initializes a new instance of the class. + /// The service provider. + /// The context. + protected EntityFrameworkUnitOfWork(IServiceProvider serviceProvider, DbContext context) + { + ServiceProvider = serviceProvider; + Context = context; + } + + /// Creates the provider-specific entity set for . + /// The entity type. + /// The provider's (a ). + protected abstract Data.Repository.ISet CreateSetCore() where TEntity : class, IEntity; + + public int Commit() + { + return Context.SaveChanges(); + } + + public int CommitAndRefreshChanges() + { + var changes = 0; + var saveFailed = false; + + do + { + try + { + changes = Context.SaveChanges(); + + saveFailed = false; + } + catch (DbUpdateConcurrencyException ex) + { + saveFailed = true; + + ex.Entries.ToList() + .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); + } + } while (saveFailed); + + return changes; + } + + public async Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) + { + var changes = 0; + var saveFailed = false; + + do + { + try + { + changes = await Context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + saveFailed = false; + } + catch (DbUpdateConcurrencyException ex) + { + saveFailed = true; + + ex.Entries.ToList() + .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); + } + } while (saveFailed); + + return changes; + } + + public async Task CommitAsync(CancellationToken cancellationToken = default) + { + return await Context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public int Commit(Action options) + { + return Commit(); + } + + public int CommitAndRefreshChanges(Action options) + { + return CommitAndRefreshChanges(); + } + + public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) + { + return CommitAndRefreshChangesAsync(cancellationToken); + } + + public Task CommitAsync(Action options, CancellationToken cancellationToken = default) + { + return CommitAsync(cancellationToken); + } + + public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity + { + ((SetBase)CreateSetCore()).ApplyCurrentValues(original, current); + } + + public void Attach(TEntity item) where TEntity : class, IEntity + { + ((SetBase)CreateSetCore()).Attach(item); + } + + public void LoadCollection(TEntity item, + Expression>> navigationProperty, + Expression> filter = null) where TEntity : class where TElement : class + { + if (filter != null) + { + Context.Entry(item).Collection(navigationProperty).Query().Where(filter).Load(); + } + else + { + Context.Entry(item).Collection(navigationProperty).Load(); + } + } + + public async Task LoadCollectionAsync(TEntity item, + Expression>> navigationProperty, + Expression> filter = null) where TEntity : class where TElement : class + { + if (filter != null) + { + await Context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); + } + else + { + await Context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); + } + } + + public void Reload(TEntity item) where TEntity : class + { + var entry = Context.Entry(item); + entry.CurrentValues.SetValues(entry.OriginalValues); + entry.Reload(); + } + + public void RollbackChanges() + { + // set all entities in change tracker as 'unchanged state' + Context?.ChangeTracker.Entries() + .ToList() + .ForEach(entry => entry.State = EntityState.Unchanged); + } + + public void SetModified(TEntity item) where TEntity : class + { + // this operation also attaches item in the object state manager + Context.Entry(item).State = EntityState.Modified; + } + + public virtual SaveOptions GetSaveOptions() + { + return new SaveOptions(); + } + + public virtual IRepository GetRepository() + where TEntity : class, IEntity + { + return ServiceProvider.GetRequiredService>(); + } + + public IAsyncRepository GetAsyncRepository() + where TEntity : class, IEntity + { + return ServiceProvider.GetRequiredService>(); + } + + public Data.Repository.ISet CreateSet() where TEntity : class, IEntity => CreateSetCore(); + + public IQueryableRepository GetQueryableRepository() + where TEntity : class, IEntity + { + return ServiceProvider.GetRequiredService>(); + } + + public IAsyncQueryableRepository GetAsyncQueryableRepository() + where TEntity : class, IEntity + { + return ServiceProvider.GetRequiredService>(); + } + + internal DbContext GetDbContext() => Context; + + /// Disposes this instance. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// Disposes the managed resources. + /// Whether to dispose managed resources. + protected virtual void Dispose(bool disposing) + { + if (Disposed) + { + return; + } + + if (disposing) + { + Context?.Dispose(); + } + + Disposed = true; + } +} + +/// +/// The strongly-typed that a document provider's unit of work +/// derives from over its concrete . +/// +/// The context type. +public abstract class EntityFrameworkUnitOfWork(IServiceProvider serviceProvider, TDbContext context) + : EntityFrameworkUnitOfWork(serviceProvider, context) + where TDbContext : DbContext; From 4b2aefde66c41c88f27a02ac0090db33e4c5864c Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 19:13:52 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=94=A7=20chore:=20bump=20base=20(4.6.?= =?UTF-8?q?0/8.3.0/10.2.0)=20and=20MongoDb=20(8.3.0/10.2.0)=20for=20the=20?= =?UTF-8?q?shared=20UnitOfWork?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The base package gained the shared EntityFrameworkUnitOfWork (additive), so it must be republished for the document providers (which now derive from it) to resolve at runtime. MongoDb is bumped to publish its thinned unit of work. The relational providers are unchanged. --- .../eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj | 6 +++--- .../eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj | 6 +++--- .../eQuantic.Core.Data.EntityFramework.Net10.csproj | 6 +++--- .../eQuantic.Core.Data.EntityFramework.Net8.csproj | 6 +++--- .../eQuantic.Core.Data.EntityFramework.csproj | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj index 2a64b88..6fb0a41 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework and Mongo DB eQuantic.Core.Data.EntityFramework.MongoDb - 10.1.0.0 + 10.2.0.0 net10.0 eQuantic.Core.Data.EntityFramework.MongoDb eQuantic.Core.Data.EntityFramework.MongoDb @@ -12,8 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 10.1.0.0 - 10.1.0.0 + 10.2.0.0 + 10.2.0.0 enable enable diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj index 76c7c37..8f03163 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework and Mongo DB eQuantic.Core.Data.EntityFramework.MongoDb - 8.2.0.0 + 8.3.0.0 net8.0 eQuantic.Core.Data.EntityFramework.MongoDb eQuantic.Core.Data.EntityFramework.MongoDb @@ -12,8 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 8.2.0.0 - 8.2.0.0 + 8.3.0.0 + 8.3.0.0 enable enable diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj index ab20736..4c4e58c 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework - 10.1.0.0 + 10.2.0.0 net10.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -12,8 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 10.1.0.0 - 10.1.0.0 + 10.2.0.0 + 10.2.0.0 diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj index ac1500b..d6d78fc 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework - 8.2.0.0 + 8.3.0.0 net8.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -12,8 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 8.2.0.0 - 8.2.0.0 + 8.3.0.0 + 8.3.0.0 diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj index 776afc9..fe4806a 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj @@ -4,15 +4,15 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework - 4.5.0.0 + 4.6.0.0 net8.0;net10.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework eQuantic;Core;Data;Library;Repository;Pattern Entity ignorant persistance with Repository Pattern for Entity Framework - 4.5.0.0 - 4.5.0.0 + 4.6.0.0 + 4.6.0.0