From 894644ba9d197e68bec714aabcef6d3416a7bbd8 Mon Sep 17 00:00:00 2001 From: Mustafa Salih ASLIM Date: Tue, 3 Sep 2024 20:01:34 +0300 Subject: [PATCH 1/5] RepositoryFactory service registeration has been improved and now transient scopd is supported --- .../Extensions/RepositoryExtensions.cs | 68 +++++++++++++++---- .../Application/Sagas/SagaTests/SagaTests.cs | 1 + .../DateTimeOffsetAsyncTests.cs | 4 ++ .../DateTimeOffsetTests.cs | 4 ++ .../UnitOfWorkTests/UnitOfWorkTests.cs | 14 ++++ .../DbContextTests/DbContextDisposeTests.cs | 4 ++ 6 files changed, 82 insertions(+), 13 deletions(-) diff --git a/src/SampleDotnet.RepositoryFactory/Extensions/RepositoryExtensions.cs b/src/SampleDotnet.RepositoryFactory/Extensions/RepositoryExtensions.cs index 1b10c04..dcbb27d 100644 --- a/src/SampleDotnet.RepositoryFactory/Extensions/RepositoryExtensions.cs +++ b/src/SampleDotnet.RepositoryFactory/Extensions/RepositoryExtensions.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace SampleDotnet.RepositoryFactory; +namespace SampleDotnet.RepositoryFactory; /// /// Extension methods for adding DbContext factory and UnitOfWork to the service collection. @@ -8,28 +6,72 @@ namespace SampleDotnet.RepositoryFactory; public static class RepositoryExtensions { /// - /// Adds a DbContext factory and a UnitOfWork to the service collection with the specified service lifetime. + /// Adds a DbContext factory to the service collection with the specified service lifetime. /// /// The type of the DbContext to add. - /// The service collection to which the services will be added. + /// The service collection to which the DbContext factory will be added. /// An optional action to configure the DbContext options. - /// The lifetime of the services to add. Default is Singleton. + /// The lifetime of the DbContext factory to add. Default is Singleton. /// The modified service collection. public static IServiceCollection AddDbContextFactoryWithUnitOfWork( this IServiceCollection serviceCollection, Action? optionsAction = null, - ServiceLifetime lifetime = ServiceLifetime.Singleton) + ServiceLifetime dbContextFactoryLifetime = ServiceLifetime.Singleton) where TContext : DbContext { // Adds a factory for creating instances of the specified DbContext type with the provided options. - Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddDbContextFactory>(serviceCollection, optionsAction, lifetime); + // The DbContextFactory is added to the service collection with the specified lifetime. + // This allows the application to create instances of DbContext on demand, using the configured options. + EntityFrameworkServiceCollectionExtensions.AddDbContextFactory>(serviceCollection, optionsAction, dbContextFactoryLifetime); - // Adds the UnitOfWork as a scoped service if it hasn't already been added. - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddScoped(serviceCollection); - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddScoped(serviceCollection); - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddScoped(serviceCollection); - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddScoped(serviceCollection); + return serviceCollection; + } + + /// + /// Adds a repository factory and associated services to the service collection with the specified service lifetime. + /// Ensures that only one instance of each service type is registered at a time by removing any existing registrations. + /// + /// The service collection to which the services will be added. + /// The lifetime of the services to add (e.g., Scoped, Transient, Singleton). + /// The modified service collection. + public static IServiceCollection AddRepositoryFactory(this IServiceCollection serviceCollection, ServiceLifetime lifetime) + { + // List of service types that need to be registered. + Type[] serviceTypes = new Type[] + { + typeof(IUnitOfWork), + typeof(IDbContextManager), + typeof(IRepositoryFactory), + typeof(ITransactionManager) + }; + + // Loop through the service collection in reverse order to remove existing registrations safely. + // This ensures that we do not modify the collection while iterating forward, which could cause errors. + for (int i = serviceCollection.Count - 1; i >= 0; i--) + { + var serviceDescriptor = serviceCollection[i]; + + // Check if the current service descriptor is one of the service types we intend to register. + if (Array.Exists(serviceTypes, type => type == serviceDescriptor.ServiceType)) + { + // Remove the existing service registration to avoid conflicts and ensure the new service is registered properly. + serviceCollection.RemoveAt(i); + } + } + + // Register a factory for creating instances of IUnitOfWork with the specified lifetime. + // This factory manually constructs the necessary dependencies (DbContextManager, RepositoryFactory, TransactionManager) + // to ensure that each IUnitOfWork instance is created with the correct dependencies. + serviceCollection.Add(new ServiceDescriptor(typeof(IUnitOfWork), x => + { + IDbContextManager dbContextManager = new DbContextManager(x); + IRepositoryFactory repositoryFactory = new Repositories.Factories.RepositoryFactory(); + ITransactionManager transactionManager = new TransactionManager(dbContextManager); + + return new UnitOfWork(dbContextManager, repositoryFactory, transactionManager); + }, lifetime)); return serviceCollection; } + } \ No newline at end of file diff --git a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Application/Sagas/SagaTests/SagaTests.cs b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Application/Sagas/SagaTests/SagaTests.cs index a6f9a0d..b1cd465 100644 --- a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Application/Sagas/SagaTests/SagaTests.cs +++ b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Application/Sagas/SagaTests/SagaTests.cs @@ -64,6 +64,7 @@ public async Task DistributedTransaction_SagaCommitAndRollback() services.AddTransient(); services.AddTransient(); + services.AddRepositoryFactory(ServiceLifetime.Transient); services.AddMassTransitTestHarness(x => { diff --git a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetAsyncTests.cs b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetAsyncTests.cs index e951463..e48ab22 100644 --- a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetAsyncTests.cs +++ b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetAsyncTests.cs @@ -42,6 +42,8 @@ public async Task Case_set_CreatedAt_DateTimeOffsetAsync() options.EnableSensitiveDataLogging(); options.EnableDetailedErrors(); }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); using (IHost build = host.Build()) @@ -91,6 +93,8 @@ public async Task Case_set_UpdatedAt_DateTimeOffsettAsync() options.EnableSensitiveDataLogging(); options.EnableDetailedErrors(); }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); using (IHost build = host.Build()) diff --git a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetTests.cs b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetTests.cs index e9bab78..391c819 100644 --- a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetTests.cs +++ b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetTests.cs @@ -42,6 +42,8 @@ public async void Case_set_CreatedAt_DateTimeOffset() options.EnableSensitiveDataLogging(); options.EnableDetailedErrors(); }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); using (IHost build = host.Build()) @@ -90,6 +92,8 @@ public async Task Case_set_UpdatedAt_DateTimeOffset() options.EnableSensitiveDataLogging(); options.EnableDetailedErrors(); }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); using (IHost build = host.Build()) diff --git a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/UnitOfWorkTests/UnitOfWorkTests.cs b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/UnitOfWorkTests/UnitOfWorkTests.cs index f496b83..c694888 100644 --- a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/UnitOfWorkTests/UnitOfWorkTests.cs +++ b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/UnitOfWorkTests/UnitOfWorkTests.cs @@ -66,6 +66,8 @@ public async Task Case_UnitOfWork_CommitAndRollback() options.EnableSensitiveDataLogging(); // Enable logging of sensitive data (for debugging purposes). options.EnableDetailedErrors(); // Enable detailed error messages (for debugging purposes). }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); // Build the IHost and get the required services for testing. @@ -248,6 +250,8 @@ public async Task Case_UnitOfWork_Do_Not_Skip_DetectChanges() options.EnableSensitiveDataLogging(); // Enable logging of sensitive data (for debugging purposes). options.EnableDetailedErrors(); // Enable detailed error messages (for debugging purposes). }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); // Build the IHost and get the required services for testing. @@ -327,6 +331,8 @@ public async Task Case_UnitOfWork_Rollback() options.EnableSensitiveDataLogging(); // Enable sensitive data logging. options.EnableDetailedErrors(); // Enable detailed error messages. }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); using (IHost build = host.Build()) @@ -454,6 +460,8 @@ public async Task Case_UnitOfWork_TwoDifferent_DbContext_Rollback() options.EnableSensitiveDataLogging(); // Enable logging of sensitive data (for debugging purposes). options.EnableDetailedErrors(); // Enable detailed error messages (for debugging purposes). }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); // Build the IHost and get the required services for testing. @@ -609,6 +617,8 @@ Verify that the entities inserted before the save point are committed and presen options.EnableSensitiveDataLogging(); options.EnableDetailedErrors(); }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); using (IHost build = host.Build()) @@ -741,6 +751,8 @@ and failure to properly handle an interleaved invalid operation could lead to da options.EnableSensitiveDataLogging(); options.EnableDetailedErrors(); }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); using (IHost build = host.Build()) @@ -832,6 +844,8 @@ public async Task Case_UnitOfWork_NestedTransactionsWithSavePoints() options.EnableSensitiveDataLogging(); // Enable logging of sensitive data (for debugging purposes). options.EnableDetailedErrors(); // Enable detailed error messages (for debugging purposes). }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); // Build the IHost and get the required services for testing. diff --git a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Infrastructure/DbContexts/DbContextTests/DbContextDisposeTests.cs b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Infrastructure/DbContexts/DbContextTests/DbContextDisposeTests.cs index dee89a4..d0d8f2b 100644 --- a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Infrastructure/DbContexts/DbContextTests/DbContextDisposeTests.cs +++ b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Infrastructure/DbContexts/DbContextTests/DbContextDisposeTests.cs @@ -42,6 +42,8 @@ public async Task Case_DbContext_Should_Not_Throw_ObjectDisposedException1() options.EnableSensitiveDataLogging(); options.EnableDetailedErrors(); }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); using (IHost build = host.Build()) @@ -101,6 +103,8 @@ public async Task Case_Repository_Should_Not_Throw_ObjectDisposedException2() options.EnableSensitiveDataLogging(); options.EnableDetailedErrors(); }); + + services.AddRepositoryFactory(ServiceLifetime.Scoped); }); using (IHost build = host.Build()) From 56890d3c45dcf621bcbec3fc573163ada3dbb887 Mon Sep 17 00:00:00 2001 From: Mustafa Salih ASLIM Date: Tue, 3 Sep 2024 20:19:58 +0300 Subject: [PATCH 2/5] improvements on service lifetimes and service injection methods --- .../Extensions/RepositoryExtensions.cs | 22 ------------------- .../Application/Sagas/SagaTests/SagaTests.cs | 4 ++-- .../DateTimeOffsetAsyncTests.cs | 4 ++-- .../DateTimeOffsetTests.cs | 4 ++-- .../UnitOfWorkTests/UnitOfWorkTests.cs | 22 +++++++++---------- .../DbContextTests/DbContextDisposeTests.cs | 4 ++-- 6 files changed, 19 insertions(+), 41 deletions(-) diff --git a/src/SampleDotnet.RepositoryFactory/Extensions/RepositoryExtensions.cs b/src/SampleDotnet.RepositoryFactory/Extensions/RepositoryExtensions.cs index dcbb27d..9c09c13 100644 --- a/src/SampleDotnet.RepositoryFactory/Extensions/RepositoryExtensions.cs +++ b/src/SampleDotnet.RepositoryFactory/Extensions/RepositoryExtensions.cs @@ -5,28 +5,6 @@ /// public static class RepositoryExtensions { - /// - /// Adds a DbContext factory to the service collection with the specified service lifetime. - /// - /// The type of the DbContext to add. - /// The service collection to which the DbContext factory will be added. - /// An optional action to configure the DbContext options. - /// The lifetime of the DbContext factory to add. Default is Singleton. - /// The modified service collection. - public static IServiceCollection AddDbContextFactoryWithUnitOfWork( - this IServiceCollection serviceCollection, - Action? optionsAction = null, - ServiceLifetime dbContextFactoryLifetime = ServiceLifetime.Singleton) - where TContext : DbContext - { - // Adds a factory for creating instances of the specified DbContext type with the provided options. - // The DbContextFactory is added to the service collection with the specified lifetime. - // This allows the application to create instances of DbContext on demand, using the configured options. - EntityFrameworkServiceCollectionExtensions.AddDbContextFactory>(serviceCollection, optionsAction, dbContextFactoryLifetime); - - return serviceCollection; - } - /// /// Adds a repository factory and associated services to the service collection with the specified service lifetime. /// Ensures that only one instance of each service type is registered at a time by removing any existing registrations. diff --git a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Application/Sagas/SagaTests/SagaTests.cs b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Application/Sagas/SagaTests/SagaTests.cs index b1cd465..d9fb2a1 100644 --- a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Application/Sagas/SagaTests/SagaTests.cs +++ b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Application/Sagas/SagaTests/SagaTests.cs @@ -35,7 +35,7 @@ public async Task DistributedTransaction_SagaCommitAndRollback() IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { // Configure CartDbContext with SQL Server settings. - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "CartDbContext_DistributedTransaction_SagaCommitAndRollback"; @@ -49,7 +49,7 @@ public async Task DistributedTransaction_SagaCommitAndRollback() }); // Configure SecondDbContext with SQL Server settings. - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "PaymentDbContext_DistributedTransaction_SagaCommitAndRollback"; // Set the initial catalog (database name). diff --git a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetAsyncTests.cs b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetAsyncTests.cs index e48ab22..169e191 100644 --- a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetAsyncTests.cs +++ b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetAsyncTests.cs @@ -29,7 +29,7 @@ public async Task Case_set_CreatedAt_DateTimeOffsetAsync() { IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "Case_set_CreatedAt_DateTimeOffsetAsync"; @@ -80,7 +80,7 @@ public async Task Case_set_UpdatedAt_DateTimeOffsettAsync() { IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "Case_set_UpdatedAt_DateTimeOffsetAsync"; diff --git a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetTests.cs b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetTests.cs index 391c819..a0b565e 100644 --- a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetTests.cs +++ b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/DateTimeOffsetTests/DateTimeOffsetTests.cs @@ -29,7 +29,7 @@ public async void Case_set_CreatedAt_DateTimeOffset() { IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "Case_set_CreatedAt_DateTimeOffset"; @@ -79,7 +79,7 @@ public async Task Case_set_UpdatedAt_DateTimeOffset() { IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "Case_set_UpdatedAt_DateTimeOffset"; diff --git a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/UnitOfWorkTests/UnitOfWorkTests.cs b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/UnitOfWorkTests/UnitOfWorkTests.cs index c694888..eed768f 100644 --- a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/UnitOfWorkTests/UnitOfWorkTests.cs +++ b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Core/Entities/UnitOfWorkTests/UnitOfWorkTests.cs @@ -40,7 +40,7 @@ public async Task Case_UnitOfWork_CommitAndRollback() IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { // Configure FirstDbContext with SQL Server settings. - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "FirstDbContext_Case_UnitOfWork_CommitAndRollback"; // Set the initial catalog (database name). @@ -54,7 +54,7 @@ public async Task Case_UnitOfWork_CommitAndRollback() }); // Configure SecondDbContext with SQL Server settings. - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "SecondDbContext_Case_UnitOfWork_CommitAndRollback"; // Set the initial catalog (database name). @@ -238,7 +238,7 @@ public async Task Case_UnitOfWork_Do_Not_Skip_DetectChanges() IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { // Configure TestApplicationDbContext with SQL Server settings. - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "Case_UnitOfWork_Do_Not_Skip_DetectChanges"; // Set the initial catalog (database name). @@ -319,7 +319,7 @@ public async Task Case_UnitOfWork_Rollback() IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { // Configure DbContextFactory and UnitOfWork for the test. - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "Case_UnitOfWork_Rollback"; // Set the database name. @@ -434,7 +434,7 @@ public async Task Case_UnitOfWork_TwoDifferent_DbContext_Rollback() IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { // Configure FirstDbContext with SQL Server settings. - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "TwoDifferent_DbContext_Rollback_FirstDb"; // Set the initial catalog (database name). @@ -448,7 +448,7 @@ public async Task Case_UnitOfWork_TwoDifferent_DbContext_Rollback() }); // Configure SecondDbContext with SQL Server settings. - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "TwoDifferent_DbContext_Rollback_SecondDb"; // Set the initial catalog (database name). @@ -592,7 +592,7 @@ Verify that the entities inserted before the save point are committed and presen // Create an IHostBuilder and configure services for testing. IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "PartialCommitAndRollback_FirstDb"; // Set the initial catalog. @@ -605,7 +605,7 @@ Verify that the entities inserted before the save point are committed and presen options.EnableDetailedErrors(); }); - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "PartialCommitAndRollback_SecondDb"; // Set the initial catalog. @@ -726,7 +726,7 @@ and failure to properly handle an interleaved invalid operation could lead to da // Create an IHostBuilder and configure services for testing. IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "InterleavedOperations_FirstDb"; // Set the initial catalog. @@ -739,7 +739,7 @@ and failure to properly handle an interleaved invalid operation could lead to da options.EnableDetailedErrors(); }); - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "InterleavedOperations_SecondDb"; // Set the initial catalog. @@ -832,7 +832,7 @@ public async Task Case_UnitOfWork_NestedTransactionsWithSavePoints() IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { // Configure TestApplicationDbContext with SQL Server settings. - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "Case_UnitOfWork_NestedTransactionsWithSavePoints"; // Set the initial catalog (database name). diff --git a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Infrastructure/DbContexts/DbContextTests/DbContextDisposeTests.cs b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Infrastructure/DbContexts/DbContextTests/DbContextDisposeTests.cs index d0d8f2b..beb82db 100644 --- a/test/SampleDotnet.RepositoryFactory.Tests/Cases/Infrastructure/DbContexts/DbContextTests/DbContextDisposeTests.cs +++ b/test/SampleDotnet.RepositoryFactory.Tests/Cases/Infrastructure/DbContexts/DbContextTests/DbContextDisposeTests.cs @@ -29,7 +29,7 @@ public async Task Case_DbContext_Should_Not_Throw_ObjectDisposedException1() { IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "Case_DbContext_Should_Not_Throw_ObjectDisposedException1"; @@ -90,7 +90,7 @@ public async Task Case_Repository_Should_Not_Throw_ObjectDisposedException2() { IHostBuilder host = Host.CreateDefaultBuilder().ConfigureServices((services) => { - services.AddDbContextFactoryWithUnitOfWork(options => + services.AddDbContextFactory(options => { var cnnBuilder = new SqlConnectionStringBuilder(_sqlContainer.GetConnectionString()); cnnBuilder.InitialCatalog = "Case_Repository_Should_Not_Throw_ObjectDisposedException2"; From 36e2c49637e694cb124a43556c1ea6c9ac5bbff5 Mon Sep 17 00:00:00 2001 From: Mustafa Salih ASLIM Date: Tue, 3 Sep 2024 20:24:27 +0300 Subject: [PATCH 3/5] Update README.md --- README.md | 199 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 154 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 15f8c15..fdd768a 100644 --- a/README.md +++ b/README.md @@ -2,66 +2,175 @@ [![CodeQL](https://github.com/msx752/SampleDotnet.RepositoryFactory/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/msx752/SampleDotnet.RepositoryFactory/actions/workflows/codeql.yml) [![MIT](https://img.shields.io/badge/License-MIT-blue.svg?maxAge=259200)](https://github.com/msx752/SampleDotnet.RepositoryFactory/blob/master/LICENSE.md) -# EFCore DbContext RepositoryFactory Pattern managed by DbContextFactory -EntityFrameworkCore doesn't support multiple parallel operations, when we need parallel actions in different threads such as adding or deleting on the same DbContext, It throws an exception when calling SaveChanges [source](https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/#avoiding-dbcontext-threading-issues). +# **SampleDotnet.RepositoryFactory Documentation** -NOTE: **DbContext service scope set as Transient which managed by IServiceScopeFactory** +## **Overview** -# How to Use -``` c# -using SampleDotnet.RepositoryFactory; +The **SampleDotnet.RepositoryFactory** NuGet package provides a robust and flexible solution for managing data access using Entity Framework Core (EF Core) in .NET applications. It leverages the **Repository Pattern** and **Unit of Work Pattern** to ensure efficient database operations, transaction management, and better separation of concerns. This package simplifies working with multiple `DbContext` instances and supports parallel operations using transient scoped `DbContexts` managed by a `DbContextFactory`. + +> **Note**: Most features in this package are currently in a preview version. While the package is stable for general use, some features may still be undergoing testing and improvements. Please provide feedback and report any issues to help us enhance the package further. + +## **Key Features** + +- **Repository Pattern**: Provides generic repositories to manage entities with CRUD operations. +- **Unit of Work Pattern**: Encapsulates transaction management and ensures all operations are completed or rolled back together. +- **DbContextFactory**: Supports transient scoped `DbContexts` to prevent concurrency issues. +- **Automatic Property Management**: Automatically updates `CreatedAt` and `UpdatedAt` properties for entities implementing `IHasDateTimeOffset`. +- **Flexible Service Lifetimes**: Supports configuring services with various lifetimes (Scoped, Transient, Singleton) to suit different application needs. + +## **Installation** + +To install the **SampleDotnet.RepositoryFactory** NuGet package, run the following command in the **Package Manager Console**: + +```shell +Install-Package SampleDotnet.RepositoryFactory -Pre ``` -ServiceCollection Definition -``` c# -services.AddDbContextFactoryWithUnitOfWork(opt => + +Or use the **.NET CLI**: + +```shell +dotnet add package SampleDotnet.RepositoryFactory --prerelease +``` + +### **Prerequisites** + +Ensure that your project targets one of the following frameworks: **.NET 6.0**, **.NET 7.0**, or **.NET 8.0**. This package does not support .NET 5. + +## **Usage** + +### **1. Service Registration** + +To use the `RepositoryFactory` and `UnitOfWork` in your application, register the necessary services in the `Startup` class or wherever you configure services in your application. + +#### **Example: Configuring Services in ASP.NET Core** + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + // Add DbContextFactory for UserDbContext with the desired lifetime + services.AddDbContextFactory(options => { - opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); + options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); }); + + // Add repository factory with a specified service lifetime "Scoped or Transient (especially for the Consumers)" + services.AddRepositoryFactory(ServiceLifetime.Scoped); + + // Additional service registrations... +} ``` -then we call transient scoped DbContext -``` c# - public class UserController : Controller - { - private readonly IUnitOfWork _unitOfWork; - public UserController(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } +This setup uses the traditional `AddDbContextFactory` method to configure `DbContextFactory` for `UserDbContext` and specifies the desired service lifetime for the factory. - [HttpDelete("{id}")] - public ActionResult Delete(Guid id) - { - using (var repository = _unitOfWork.CreateRepository()) - { - var personal = repository.FirstOrDefault(f => f.Id == id); +### **2. Using the Repository and Unit of Work in Controllers** - //some operations goes here.... +After configuring the services, use the `IUnitOfWork` and repository pattern in your controllers to manage database operations. - repository.Delete(personal); +#### **Example: UserController** - //some operations goes here.... - } +```csharp +public class UserController : Controller +{ + private readonly IUnitOfWork _unitOfWork; - _unitOfWork.SaveChanges(); - return Ok(); - } + public UserController(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; } -``` -# Additional Feature -- If `IHasDateTimeOffset` interfece used on Entity object then value of the the CreatedAt and UpdatedAt properties will be updated automatically. -``` c# - public class TestUserEntity : IHasDateTimeOffset + [HttpDelete("{id}")] + public ActionResult Delete(Guid id) + { + using (var repository = _unitOfWork.CreateRepository()) { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Guid Id { get; set; } - public string Name { get; set; } - public string Surname { get; set; } - - public DateTimeOffset? CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + var user = repository.FirstOrDefault(u => u.Id == id); + + // Perform operations on the entity... + + repository.Delete(user); + + // Additional operations... } + + _unitOfWork.SaveChanges(); + return Ok(); + } +} +``` + +### **3. Automatic Timestamps for Entities** + +If your entities implement the `IHasDateTimeOffset` interface, their `CreatedAt` and `UpdatedAt` properties will be automatically managed when using repository methods. + +#### **Example: TestUserEntity** + +```csharp +public class TestUserEntity : IHasDateTimeOffset +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public string Name { get; set; } + public string Surname { get; set; } + + public DateTimeOffset? CreatedAt { get; set; } + public DateTimeOffset? UpdatedAt { get; set; } +} ``` +### **4. Parallel Operations and DbContextFactory** + +The package supports parallel operations by using a `DbContextFactory` to create new `DbContext` instances. This approach prevents concurrency issues that occur when the same `DbContext` instance is used across multiple threads. + +## **Special Note on Transaction Management** + +> **IMPORTANT**: Proper transaction management is crucial when working with multiple `DbContext` instances. + +### **Key Points to Consider:** + +- **Using Multiple DbContexts**: If you are using multiple `DbContext` instances, each managing its own transaction, be aware that if a transaction commits successfully on one `DbContext` but fails on another, you may need to manually roll back already committed transactions to maintain data consistency. This scenario requires careful handling to ensure that all related changes are either fully committed or completely rolled back across all DbContexts. + +- **Using a Single DbContext**: When using a single `DbContext` instance for all operations within a unit of work, transaction management is simplified. All operations are either committed together or rolled back together, eliminating the need to manually roll back already committed transactions from different `DbContext` instances. + +### **Best Practice**: To avoid the complexity of handling multiple transactions, use a single `DbContext` instance within the scope of a unit of work whenever possible. This approach ensures straightforward transaction management and helps maintain data integrity. + +## **Descriptions of Key Components** + +### **IUnitOfWork Interface** + +The `IUnitOfWork` interface defines methods for managing database transactions and repositories: +- **`CreateRepository()`**: Creates a new repository for the specified `DbContext`. +- **`SaveChanges()`**: Commits all pending changes to the database. +- **`SaveChangesAsync()`**: Asynchronously commits all pending changes to the database. +- **`IsDbConcurrencyExceptionThrown`**: Indicates if a concurrency exception occurred. +- **`SaveChangesException`**: Provides details about any exception thrown during the save operation. + +### **Repository Class** + +A generic repository class that provides CRUD operations and additional methods for querying and manipulating entities: +- **`AsQueryable()`**: Returns an `IQueryable` for the specified entity type. +- **`Delete()`**: Deletes an entity. +- **`Find()` and `FindAsync()`**: Finds entities by primary key values. +- **`Insert()` and `InsertAsync()`**: Inserts new entities. +- **`Update()`**: Updates entities. + +### **DbContextFactory and Service Lifetimes** + +The `DbContextFactory` can now be configured with different lifetimes (Scoped, Transient, Singleton) based on your application's needs. Using a transient lifetime is particularly useful for avoiding concurrency issues in multi-threaded environments. + +### **Transaction Management** + +The `UnitOfWork` and `TransactionManager` handle transaction management, ensuring that all operations within a unit of work are either committed or rolled back together. + +## **Best Practices** + +- **Use Scoped or Transient Services**: Use scoped or transient services for the `UnitOfWork` and related services depending on your application's concurrency requirements. +- **Avoid Long-Running Operations**: Keep the `DbContext` lifespan short to avoid holding resources longer than necessary. +- **Handle Exceptions Gracefully**: Use `IsDbConcurrencyExceptionThrown` and `SaveChangesException` to handle concurrency and other exceptions effectively. + +## **Conclusion** + +The **SampleDotnet.RepositoryFactory** NuGet package provides a streamlined way to manage data access in .NET applications, leveraging best practices for transaction management and parallel operations. With support for various service lifetimes, you can now tailor the package to suit your application's specific needs. By following this guide, you can easily integrate this package into your projects and take advantage of its robust features. + +For more details or support, please refer to the package documentation or reach out to the community for assistance. From d3e2c0ef624436216d72a0505ff0786d019d9ab5 Mon Sep 17 00:00:00 2001 From: Mustafa Salih ASLIM Date: Tue, 3 Sep 2024 20:33:08 +0300 Subject: [PATCH 4/5] nuget package version upgraded to 3.1.0-preview2 --- .../SampleDotnet.RepositoryFactory.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SampleDotnet.RepositoryFactory/SampleDotnet.RepositoryFactory.csproj b/src/SampleDotnet.RepositoryFactory/SampleDotnet.RepositoryFactory.csproj index 8d5e5ab..9859aba 100644 --- a/src/SampleDotnet.RepositoryFactory/SampleDotnet.RepositoryFactory.csproj +++ b/src/SampleDotnet.RepositoryFactory/SampleDotnet.RepositoryFactory.csproj @@ -11,8 +11,8 @@ True SampleDotnet.RepositoryFactory SampleDotnet.RepositoryFactory - EFCore UnitOfWork Pattern used on DbContextFactory for the multiple DbContexts - + EF Core Unit of Work pattern combined with DbContextFactory to efficiently manage multiple DbContexts in .NET applications. This package supports various service lifetimes (Scoped, Transient, Singleton) and provides a flexible, robust solution for handling database operations with automatic transaction management and repository patterns. Ideal for projects requiring concurrent data access and simplified entity management. + The SampleDotnet.RepositoryFactory package now uses the standard AddDbContextFactory method for registering DbContext factories and supports configurable service lifetimes (Scoped, Transient, Singleton). Most features are in preview, and feedback is encouraged. Documentation has been updated with new service registration examples and transaction management best practices. Mustafa Salih ASLIM; https://github.com/msx752/SampleDotnet.RepositoryFactory README.md @@ -21,7 +21,7 @@ True False wfc2.png - dotnet net6 net7 net9 unitofwork pattern repository factory multiple dbcontexts + dotnet net6 net7 net8 unit-of-work repository-pattern factory-pattern multiple-dbcontexts LICENSE False True @@ -31,8 +31,8 @@ True True Copyright 2023 - 3.0.3.0 - 3.0.3-preview1 + 3.1.0.0 + 3.1.0-preview2 True From 0523774b50860e73328f689aac7baca03f48c9b3 Mon Sep 17 00:00:00 2001 From: Mustafa Salih ASLIM Date: Tue, 3 Sep 2024 21:54:58 +0300 Subject: [PATCH 5/5] removed extra params --- .github/workflows/dotnet.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 9691c58..6badae1 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -34,10 +34,10 @@ jobs: run: dotnet restore - name: Build - run: dotnet build -c Release --no-restore + run: dotnet build -c Release - name: Test - run: dotnet test test/SampleDotnet.RepositoryFactory.Tests/SampleDotnet.RepositoryFactory.Tests.csproj --no-restore --verbosity normal + run: dotnet test test/SampleDotnet.RepositoryFactory.Tests/SampleDotnet.RepositoryFactory.Tests.csproj --verbosity normal - name: Publish id: SampleDotnet_RepositoryFactory