The Generic Repository pattern is used to simplify data access operations in .NET Core applications. This package offers support for CRUD operations, custom queries, Unit of Work for transaction management, pagination, dynamic ordering, and multiple DbContext support.
- CRUD operations with both sync and async support.
- Custom queries via LINQ and
Expressiontrees. IIncludableQueryable-based include API — supportsThenInclude, filtered includes, etc.- Pagination through
PagingRequest/PagingResult<T>andIQueryable.ToPagedAsync()extension. - Dynamic ordering with
OrderByProperty("Author.Name")— supports nested properties. - Unit of Work generic per
DbContext(IUnitOfWork<TContext>) with transaction management. - Multiple DbContext support — register repositories and unit of works per context, no collisions.
- Marker interfaces — auto-detected so you can inject
IAppUnitOfWorkinstead ofIUnitOfWork<ApplicationDbContext>.
dotnet add package MGenericRepository// 1. (Optional) Create a base repository for your application to avoid repeating ApplicationDbContext
public class ApplicationRepository<TEntity> : Repository<TEntity, ApplicationDbContext>
where TEntity : class
{
public ApplicationRepository(ApplicationDbContext context) : base(context) { }
}
// 2. Define your entity-specific repository
public interface IProductRepository : IRepository<Product>
{
}
public class ProductRepository : ApplicationRepository<Product>, IProductRepository
{
public ProductRepository(ApplicationDbContext context) : base(context) { }
}The Repository<TEntity, TContext> base class binds your repository to a specific DbContext.
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddGenericRepository<ApplicationDbContext>(options =>
{
// Optional — defaults to the calling assembly if omitted
// options.RegisterServicesFromAssembly(typeof(ProductRepository).Assembly);
});In layered solutions, the interface lives in the Application layer and the implementation lives in the Persistence/Infrastructure layer. Both assemblies must be scanned:
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddGenericRepository<ApplicationDbContext>(options =>
{
var applicationAssembly = typeof(IProductRepository).Assembly;
var persistenceAssembly = typeof(ProductRepository).Assembly;
options.RegisterServicesFromAssemblies(applicationAssembly, persistenceAssembly);
});You can call AddGenericRepository<TContext> multiple times, once per context:
builder.Services.AddDbContext<ApplicationDbContext>(opt => opt.UseSqlServer(appConn));
builder.Services.AddDbContext<CatalogDbContext>(opt => opt.UseNpgsql(catalogConn));
builder.Services.AddGenericRepository<ApplicationDbContext>(options =>
options.RegisterServicesFromAssemblies(
typeof(IProductRepository).Assembly,
typeof(ProductRepository).Assembly));
builder.Services.AddGenericRepository<CatalogDbContext>(options =>
options.RegisterServicesFromAssemblies(
typeof(ICategoryRepository).Assembly,
typeof(CategoryRepository).Assembly));Each Repository<TEntity, TContext> is registered only against the matching context — no collisions.
Inject IUnitOfWork<TContext> directly:
public class ProductService
{
private readonly IProductRepository _repository;
private readonly IUnitOfWork<ApplicationDbContext> _unitOfWork;
public ProductService(
IProductRepository repository,
IUnitOfWork<ApplicationDbContext> unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
public async Task<Product> AddProductAsync(Product product, CancellationToken ct)
{
await _repository.AddAsync(product, ct);
await _unitOfWork.SaveChangesAsync(ct);
return product;
}
}Writing IUnitOfWork<ApplicationDbContext> everywhere is verbose. Define a marker interface in your Application layer — no class needed, the package auto-registers it:
// Application layer
public interface IAppUnitOfWork : IUnitOfWork<ApplicationDbContext> { }
public interface ICatalogUnitOfWork : IUnitOfWork<CatalogDbContext> { }That's it. No implementation class to write — the package binds these markers to the correct UnitOfWork<TContext> automatically during DI registration.
public class OrderService
{
public OrderService(IAppUnitOfWork appUow, ICatalogUnitOfWork catalogUow) { ... }
}Make sure the assembly containing your marker interfaces is registered via
RegisterServicesFromAssembly(...)so the scanner can find them.
Include uses the IIncludableQueryable pattern, so ThenInclude works:
var product = await _repository.GetFirstOrDefaultAsyncNoTracking(
p => p.Id == id,
include: q => q.Include(p => p.Category)
.Include(p => p.Reviews).ThenInclude(r => r.User),
cancellationToken: ct);var products = await _repository.GetListAsync(
filter: p => p.IsActive,
include: q => q.Include(p => p.Category),
cancellationToken: ct);When the built-in helpers aren't enough, use Query() to get an IQueryable<T> and compose freely:
var query = _repository.Query(
filter: p => p.Price > 100,
include: q => q.Include(p => p.Category));
var topProducts = await query
.OrderByDescending(p => p.SalesCount)
.Take(10)
.ToListAsync(ct);var request = new PagingRequest
{
PageNumber = 1,
PageSize = 20,
OrderBy = "CreateAppUser.FullName", // nested property paths supported
IsDesc = true
};
var page = await _repository.GetPagedAsync(
request,
filter: p => p.IsActive,
include: q => q.Include(p => p.CreateAppUser),
cancellationToken: ct);
// page.Items, page.TotalCount, page.PageNumber, page.PageSize
// page.TotalPages, page.HasPrevious, page.HasNextIf you already have a custom query, paginate it with the extension:
using GenericRepository.Extensions;
var page = await _context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.Include(p => p.Category)
.ToPagedAsync(new PagingRequest
{
PageNumber = 1,
PageSize = 20,
OrderBy = "Name"
}, ct);Pass PageSize = QueryableExtensions.AllRecords (or -1) to skip paging and return everything:
var all = await _repository.GetPagedAsync(
new PagingRequest
{
PageNumber = 1,
PageSize = QueryableExtensions.AllRecords
});using GenericRepository.Extensions;
var ordered = _context.Products
.OrderByProperty("Category.Name", descending: true);public async Task TransferAsync(Guid fromId, Guid toId, decimal amount, CancellationToken ct)
{
await _unitOfWork.BeginTransactionAsync(ct);
try
{
// ... your work ...
await _unitOfWork.CommitAsync(ct);
}
catch
{
await _unitOfWork.RollbackAsync(ct);
throw;
}
}Insert in batches with AddRangeAsync:
await _repository.AddRangeAsync(products, ct);
await _unitOfWork.SaveChangesAsync(ct);For bulk update / delete in a single round-trip, use EF Core's native
ExecuteUpdateAsync / ExecuteDeleteAsync directly on Query():
await _repository.Query(p => p.IsDiscontinued)
.ExecuteUpdateAsync(s => s.SetProperty(p => p.IsActive, false), ct);
await _repository.Query(p => p.IsDiscontinued)
.ExecuteDeleteAsync(ct);| Method | Purpose |
|---|---|
Add / AddAsync / AddRange / AddRangeAsync |
Insert entities (Sync/Async) |
Update / UpdateRange |
Update entities |
Delete / DeleteRange |
Remove entities |
GetFirstOrDefault / GetFirstOrDefaultAsync |
Get single entity by predicate |
GetFirstOrDefaultAsNoTracking / GetFirstOrDefaultAsyncNoTracking |
Get single entity (No Tracking) |
GetListAsync / GetListAsyncNoTracking |
List of entities by predicate |
Query |
Compose your own IQueryable<T> with optional tracking |
GetPagedAsync |
Paginated result with optional tracking |
Any / AnyAsync |
Check existence |
CountBy |
Count entities matching a predicate |
IUnitOfWork<TContext> |
BeginTransactionAsync, CommitAsync, RollbackAsync, SaveChangesAsync, SaveChanges |
IUnitOfWorkis now generic:IUnitOfWork<TContext>. Update all injection sites.AddGenericRepository(...)is now generic:AddGenericRepository<TContext>(...). TheUseDbContext<T>()option is removed — pass the context as a generic parameter instead.- Include parameters now use
Func<IQueryable<T>, IIncludableQueryable<T, object>>instead ofparams Expression<Func<T, object>>[]. Rewrite includes as lambdas:q => q.Include(...).ThenInclude(...). GetByExpression/GetByExpressionAsyncremoved — useGetFirstOrDefault/GetFirstOrDefaultAsync(identical behavior).First/FirstAsync/GetFirst/GetFirstAsyncremoved — useGetFirstOrDefault/GetFirstOrDefaultAsyncand handle null explicitly, or callQuery().First().- Sync
Whereremoved — useQuery(filter: ...).