Skip to content
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

new version 3.1.0-preview2 #25

Merged
merged 5 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
199 changes: 154 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserDbContext>(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<UserDbContext>(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<UserDbContext>())
{
var personal = repository.FirstOrDefault<UserEntity>(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<UserDbContext>())
{
[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<UserEntity>(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<TDbContext>()`**: 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<TDbContext> Class**

A generic repository class that provides CRUD operations and additional methods for querying and manipulating entities:
- **`AsQueryable<T>()`**: Returns an `IQueryable` for the specified entity type.
- **`Delete<T>()`**: Deletes an entity.
- **`Find<T>()` and `FindAsync<T>()`**: Finds entities by primary key values.
- **`Insert<T>()` and `InsertAsync<T>()`**: Inserts new entities.
- **`Update<T>()`**: 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.
Original file line number Diff line number Diff line change
@@ -1,35 +1,55 @@
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace SampleDotnet.RepositoryFactory;
namespace SampleDotnet.RepositoryFactory;

/// <summary>
/// Extension methods for adding DbContext factory and UnitOfWork to the service collection.
/// </summary>
public static class RepositoryExtensions
{
/// <summary>
/// Adds a DbContext factory and a UnitOfWork to the service collection with the specified service lifetime.
/// 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.
/// </summary>
/// <typeparam name="TContext">The type of the DbContext to add.</typeparam>
/// <param name="serviceCollection">The service collection to which the services will be added.</param>
/// <param name="optionsAction">An optional action to configure the DbContext options.</param>
/// <param name="lifetime">The lifetime of the services to add. Default is Singleton.</param>
/// <param name="lifetime">The lifetime of the services to add (e.g., Scoped, Transient, Singleton).</param>
/// <returns>The modified service collection.</returns>
public static IServiceCollection AddDbContextFactoryWithUnitOfWork<TContext>(
this IServiceCollection serviceCollection,
Action<DbContextOptionsBuilder>? optionsAction = null,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
where TContext : DbContext
public static IServiceCollection AddRepositoryFactory(this IServiceCollection serviceCollection, ServiceLifetime lifetime)
{
// Adds a factory for creating instances of the specified DbContext type with the provided options.
Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddDbContextFactory<TContext, Microsoft.EntityFrameworkCore.Internal.DbContextFactory<TContext>>(serviceCollection, optionsAction, 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);
}
}

// Adds the UnitOfWork as a scoped service if it hasn't already been added.
Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddScoped<IUnitOfWork, UnitOfWork>(serviceCollection);
Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddScoped<IDbContextManager, DbContextManager>(serviceCollection);
Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddScoped<IRepositoryFactory, Repositories.Factories.RepositoryFactory>(serviceCollection);
Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddScoped<ITransactionManager, TransactionManager>(serviceCollection);
// 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<id>SampleDotnet.RepositoryFactory</id>
<Title>SampleDotnet.RepositoryFactory</Title>
<Description>EFCore UnitOfWork Pattern used on DbContextFactory for the multiple DbContexts</Description>
<Summary></Summary>
<Description>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.</Description>
<Summary>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.</Summary>
<Authors>Mustafa Salih ASLIM;</Authors>
<PackageProjectUrl>https://github.com/msx752/SampleDotnet.RepositoryFactory</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand All @@ -21,7 +21,7 @@
<EnableNETAnalyzers>True</EnableNETAnalyzers>
<SignAssembly>False</SignAssembly>
<PackageIcon>wfc2.png</PackageIcon>
<PackageTags>dotnet net6 net7 net9 unitofwork pattern repository factory multiple dbcontexts</PackageTags>
<PackageTags>dotnet net6 net7 net8 unit-of-work repository-pattern factory-pattern multiple-dbcontexts</PackageTags>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<CheckForOverflowUnderflow>False</CheckForOverflowUnderflow>
<IncludeSymbols>True</IncludeSymbols>
Expand All @@ -31,8 +31,8 @@
<ContinuousIntegrationBuild>True</ContinuousIntegrationBuild>
<EmbedUntrackedSources>True</EmbedUntrackedSources>
<Copyright>Copyright 2023</Copyright>
<AssemblyVersion>3.0.3.0</AssemblyVersion>
<Version>3.0.3-preview1</Version>
<AssemblyVersion>3.1.0.0</AssemblyVersion>
<Version>3.1.0-preview2</Version>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>

Expand Down
Loading
Loading