A lightweight, flexible and extensible implementation of the Repository and Unit of Work patterns using Entity Framework Core.
Designed for clean architecture, testability, and ease of integration across any .NET application.
- Generic
IRepository<T>
andIReadOnlyRepository<T>
IUnitOfWork
with transaction support- Audit support (
CreatedOn
,UpdatedOn
,DeletedOn
,CreatedBy
, etc.) - Soft delete support
- Actor context integration for automatic user tracking
- Global
DbContextBase
with auto-audit and soft-delete filters - Pagination and filtering utilities
- Repository model configuration via
UnitOfWorkOptions
(including table name pluralization using Humanizer) - Ready-to-use in-memory tests
dotnet add package Nerv.Repository
public class User : Entity<Guid>
{
public string Name { get; set; }
}
public class AppDbContext : DbContextBase<Guid, AppDbContext>
{
public AppDbContext(
DbContextOptions<AppDbContext> options,
ActorContext<Guid> actor,
UnitOfWorkOptions uowOptions) : base(options, actor, uowOptions)
{
}
}
var builder = WebApplication.CreateBuilder(args);
// Register IHttpContextAccessor (needed to access HTTP context)
builder.Services.AddHttpContextAccessor();
// Register everything using the AddRepositoryPattern extension
builder.Services.AddRepositoryPattern<AppDbContext, Guid>(
// Define the context name in a multi-database scenario,
// must be used also in a single database scenario
contextName: "Main",
// DbContext configuration
options =>
{
// Retrieve connection string from configuration
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
// Configure EF Core provider (in this case, SQL Server)
options.UseSqlServer(connectionString);
},
// ActorContext factory configuration (used for audit tracking)
provider =>
{
// Resolve IHttpContextAccessor to access the current HTTP context
var httpContext = provider.GetRequiredService<IHttpContextAccessor>()?.HttpContext;
// Extract the user ID from claims (e.g. OpenID Connect "sub" claim)
var userIdClaim = httpContext?.User?.FindFirst("sub")?.Value;
// Parse the user ID or fallback to Guid.Empty if not authenticated
Guid userId = userIdClaim != null
? Guid.Parse(userIdClaim)
: Guid.Empty;
// Return ActorContext to be injected into DbContextBase
return new ActorContext<Guid> { UserId = userId };
},
// UnitOfWork options configuration (repository model customization)
uowOptions =>
{
// Enable pluralization of table names (e.g. User -> Users)
uowOptions.ModelOptions.UsePluralization = true;
}
);
// Register UnitOfWorkFactory only once
builder.services.AddUnitOfWorkFactory();
You only need to inject IUnitOfWorkFactory
in your services. All unit of work are accessed through it:
public class UserService
{
private readonly IUnitOfWork _unitOfWork;
public UserService(IUnitOfWorkFactory factory)
{
_unitOfWork = factory.GetUnitOfWork("Main");
}
public async Task CreateAsync()
{
var userRepo = _unitOfWork.Repository<User>();
await userRepo.AddAsync(new User { Id = Guid.NewGuid(), Name = "Test" });
await _unitOfWork.SaveChangesAsync();
}
}
All changes are tracked by ActorContext<TUserId>
:
public class ActorContext<TUserId>
{
public TUserId UserId { get; set; }
}
You can test your repositories and unit of work using the in-memory SQLite provider.
public class UserTests
{
[Fact]
public async Task Add_Should_Persist_User()
{
using var fixture = RepositoryFixture.Create();
var user = new User { Id = Guid.NewGuid(), Name = "Test" };
await fixture.Repository.AddAsync(user);
await fixture.UnitOfWork.SaveChangesAsync();
var result = await fixture.Repository.GetByIdAsync(user.Id);
Assert.NotNull(result);
}
}
This project is licensed under the MIT License.
Feel free to open issues or pull requests. Contributions are welcome!
- Entity Framework Core
- Humanizer — Used for automatic pluralization of table names