MrBin.Repositories
1.0.8
dotnet add package MrBin.Repositories --version 1.0.8
NuGet\Install-Package MrBin.Repositories -Version 1.0.8
<PackageReference Include="MrBin.Repositories" Version="1.0.8" />
<PackageVersion Include="MrBin.Repositories" Version="1.0.8" />
<PackageReference Include="MrBin.Repositories" />
paket add MrBin.Repositories --version 1.0.8
#r "nuget: MrBin.Repositories, 1.0.8"
#:package MrBin.Repositories@1.0.8
#addin nuget:?package=MrBin.Repositories&version=1.0.8
#tool nuget:?package=MrBin.Repositories&version=1.0.8
MrBin Repositories
A .NET 9 library implementing the Unit of Work and Repository patterns on top of Entity Framework Core, with support for:
- Generic repositories (CRUD + Upsert) for simple and composite keys
- Pre/post-save event system with DI handlers
- Specification pattern for composable queries
- SQL Connector for raw queries
- DI registration via a single extension method
Installation
The project is structured into two main packages:
MrBin.Repositories.Core— core interfaces and implementationsMrBin.Repositories— DI registration extensions
Setup
1. Define the DbContext
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
public AppDbContext(DbContextOptions options) : base(options) { }
}
2. Define your UnitOfWork interface (optional)
public interface IAppUnitOfWork : IUnitOfWork<AppDbContext> { }
public class AppUnitOfWork : UnitOfWork<AppDbContext>, IAppUnitOfWork
{
public AppUnitOfWork(UnitOfWorkOptions<AppDbContext> options) : base(options) { }
}
3. Register services
services.AddDbContextFactory<AppDbContext>(opt =>
opt.UseSqlServer(connectionString));
services.AddUnitOfWork<AppDbContext, IAppUnitOfWork, AppUnitOfWork>(opt =>
{
opt.WithDbContextFactory(sp =>
sp.GetRequiredService<IDbContextFactory<AppDbContext>>().CreateDbContext());
opt.WithEvents(); // enable the event system
});
If you don't need a custom interface, you can register directly:
services.AddUnitOfWork<AppDbContext>(opt =>
{
opt.WithDbContextFactory(sp =>
sp.GetRequiredService<IDbContextFactory<AppDbContext>>().CreateDbContext());
});
Basic usage
Inject and CRUD operations
public class ProductService
{
private readonly IAppUnitOfWork _unitOfWork;
public ProductService(IAppUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task CreateProductAsync(Product product)
{
var repo = _unitOfWork.Write<Product, int>();
await repo.AddAsync(product);
await _unitOfWork.CommitAsync();
}
public Product? GetProduct(int id)
{
return _unitOfWork.Read<Product, int>().Get(id);
}
public async Task UpdateProductAsync(Product product)
{
var repo = _unitOfWork.Write<Product, int>();
await repo.UpdateAsync(product);
await _unitOfWork.CommitAsync();
}
public async Task DeleteProductAsync(int id)
{
var repo = _unitOfWork.Write<Product, int>();
await repo.DeleteAsync(id);
await _unitOfWork.CommitAsync();
}
}
Non-generic repository (when the type is only known at runtime)
_unitOfWork.WriteRepository.Add<Product>(product);
_unitOfWork.ReadOnlyRepository.Get<Product, int>(42);
_unitOfWork.Commit();
Upsert
Inserts if the entity does not exist, updates if it does (lookup is performed by primary key):
var repo = _unitOfWork.Write<Product, int>();
repo.Upsert(product); // single
repo.Upsert(product1, product2); // batch
_unitOfWork.Commit();
Batch operations
var repo = _unitOfWork.Write<Product, int>();
await repo.AddAsync(list); // List<Product>
await repo.DeleteAsync(keys); // List<int>
await _unitOfWork.CommitAsync();
Composite keys
For entities with a composite key (e.g. Code + Market), define a key interface and configure EF Core:
public interface IProductKey
{
string Code { get; set; }
string Market { get; set; }
}
public class Product : IProductKey
{
public string Code { get; set; }
public string Market { get; set; }
public string Name { get; set; }
// Virtual [NotMapped] property to expose the composite key
[NotMapped]
public IProductKey Id
{
get => new ProductKey { Code = Code, Market = Market };
set { Code = value.Code; Market = value.Market; }
}
}
// EF configuration
modelBuilder.Entity<Product>().HasKey(e => new { e.Code, e.Market });
Usage:
var repo = _unitOfWork.Write<Product, IProductKey>();
repo.Upsert(product);
_unitOfWork.Commit();
var found = _unitOfWork.Read<Product, IProductKey>()
.Get(new ProductKey { Code = "ABC", Market = "IT" });
Queries
By predicate
var readRepo = _unitOfWork.Read<Product, int>();
var expensiveProducts = readRepo
.Get(p => p.Price > 100)
.ToList();
With query delegate
var products = readRepo
.Get(q => q.Where(p => p.IsActive).OrderBy(p => p.Name).Take(50))
.ToList();
By multiple keys
var products = readRepo.Get(1, 2, 3).ToList(); // IQueryable<Product>
Transactions
_unitOfWork.BeginTransaction();
try
{
_unitOfWork.WriteRepository.Add(entity1);
_unitOfWork.Commit();
_unitOfWork.WriteRepository.Add(entity2);
_unitOfWork.Commit();
// CommitTransaction is handled implicitly when leaving the scope
}
catch
{
_unitOfWork.Rollback();
throw;
}
Specification Pattern
Allows defining reusable and composable filter criteria:
public class ActiveProductsSpec : Specification<Product>
{
public ActiveProductsSpec()
{
AddCondition(p => p.IsActive);
}
}
public class ByMarketSpec : Specification<Product>
{
public ByMarketSpec(string market)
{
AddCondition(p => p.Market == market);
}
}
Usage:
var activeSpec = new ActiveProductsSpec();
// Apply a single specification
var activeProducts = _unitOfWork.ReadOnlyRepository
.FromSpecification(activeSpec)
.ToList();
// Compose via LINQ after the first specification
var activeItalianProducts = _unitOfWork.ReadOnlyRepository
.FromSpecification(activeSpec)
.Where(p => p.Market == "IT")
.ToList();
Built-in specifications
| Class | Behavior |
|---|---|
AllSpecification<T> |
No filter, returns everything |
FalseSpecification<T> |
Always returns zero results |
NullFilterSpecification<T> |
Ignores null condition, returns everything |
GenericQuerySpecification<T> |
Wraps a query delegate |
Event System
Allows hooking logic before and after saves, per entity or per the entire context.
Event types
| Event | When |
|---|---|
BeforeSaveEvent<TEntity> |
Before SaveChanges, for each modified entity |
AfterSaveEvent<TEntity> |
After SaveChanges, for each saved entity |
BeforeContextSaveEvent<TContext> |
Before SaveChanges, once per commit |
AfterContextSaveEvent<TContext> |
After SaveChanges, once per commit |
SaveEvent<TEntity> also exposes an Operation property (Add, Update, Delete).
Implementing a handler
public class ProductAuditHandler :
IUnitOfWorkEventHandler<AppDbContext, BeforeSaveEvent<Product>>,
IUnitOfWorkEventHandler<AppDbContext, AfterSaveEvent<Product>>
{
private readonly IAuditService _audit;
public ProductAuditHandler(IAuditService audit)
{
_audit = audit;
}
public Task HandleAsync(BeforeSaveEvent<Product> @event, CancellationToken ct = default)
{
// Entity is still in memory: can be modified
@event.Entity.UpdatedAt = DateTime.UtcNow;
return Task.CompletedTask;
}
public Task HandleAsync(AfterSaveEvent<Product> @event, CancellationToken ct = default)
{
// Entity is persisted: side effects, notifications, etc.
return _audit.LogAsync(@event.Entity, @event.Operation, ct);
}
}
Handlers are automatically discovered via assembly scanning when opt.WithEvents() is called during registration. No manual registration is required.
SQL Connector
For raw queries when EF Core is not sufficient:
public class ReportService
{
private readonly ISqlConnector _sql;
public ReportService(ISqlConnector sql)
{
_sql = sql;
}
public IEnumerable<ProductSummary> GetSummary(string market)
{
return _sql.Select<ProductSummary>(
"SELECT Code, Name, Price FROM Products WHERE Market = @market",
new { market });
}
public ProductSummary? GetOne(string code)
{
return _sql.SelectOne<ProductSummary>(
"SELECT * FROM Products WHERE Code = @code",
new { code });
}
public void BulkInsert(IEnumerable<Product> products)
{
_sql.BulkExecute(
"INSERT INTO Products (Code, Market, Name) VALUES (@Code, @Market, @Name)",
products);
}
}
The SqlConnector is obtained via UnitTestDependencies.BuildSqlConnector(dbConnection) or by registering it in the DI container with your own DbConnection.
Manual transactions with SqlConnector
_sql.BeginTransaction();
try
{
_sql.Execute("INSERT INTO ...", params1);
_sql.Execute("UPDATE ...", params2);
_sql.CommitTransaction();
}
catch
{
_sql.RollbackTransaction();
throw;
}
Project structure
src/
├── MrBin.Core/ # Base library (Strategies, core interfaces)
├── Repositories/
│ ├── MrBin.Repositories.Core/ # Interfaces, UnitOfWork, Repository, events
│ └── MrBin.Repositories/ # DI registration (Extensions.cs)
└── Utilities/
├── MrBin.Utilities/ # Helpers, ObjectExtensions, AsyncHelper
├── MrBin.Utilities.Extensions/ # LINQ and IQueryable extensions
└── MrBin.Utilities.Types.Builders/ # Dynamic runtime type generation
tests/
├── MRBin.Repositories.Tests.Shared/ # Shared models and utilities
├── MrBin.Repositories.Tests/ # Integration tests
└── MrBin.Repositories.Tests.UnitsTests/ # Unit tests
Tech stack
| Dependency | Version | Purpose |
|---|---|---|
| .NET | 9.0 | Target framework |
| Entity Framework Core | 9.0.14 | ORM |
| EF Core SQLite | 9.0.14 | Database for tests |
| EF Core InMemory | 9.0.14 | In-memory database for tests |
| Microsoft.Extensions.DependencyInjection | 9.0.14 | DI container |
| Scrutor | 6.1.0 | Assembly scanning for handlers |
| NUnit | 4.5.1 | Test framework |
| FluentAssertions | 8.9.0 | Test assertions |
| Bogus | 35.6.5 | Fake data generation for tests |
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- MrBin.Repositories.Core (>= 1.0.8)
- Scrutor (>= 6.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.