MrBin.Repositories 1.0.8

dotnet add package MrBin.Repositories --version 1.0.8
                    
NuGet\Install-Package MrBin.Repositories -Version 1.0.8
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MrBin.Repositories" Version="1.0.8" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MrBin.Repositories" Version="1.0.8" />
                    
Directory.Packages.props
<PackageReference Include="MrBin.Repositories" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MrBin.Repositories --version 1.0.8
                    
#r "nuget: MrBin.Repositories, 1.0.8"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MrBin.Repositories@1.0.8
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MrBin.Repositories&version=1.0.8
                    
Install as a Cake Addin
#tool nuget:?package=MrBin.Repositories&version=1.0.8
                    
Install as a Cake Tool

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 implementations
  • MrBin.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.8 122 6/2/2026
1.0.7 111 6/2/2026
1.0.6 111 6/2/2026
1.0.5 105 4/8/2026
1.0.4 114 4/8/2026
1.0.3 108 3/26/2026
1.0.2 107 3/26/2026
1.0.1 104 3/26/2026