Common.EntityFramework
1.0.3
dotnet add package Common.EntityFramework --version 1.0.3
NuGet\Install-Package Common.EntityFramework -Version 1.0.3
<PackageReference Include="Common.EntityFramework" Version="1.0.3" />
<PackageVersion Include="Common.EntityFramework" Version="1.0.3" />
<PackageReference Include="Common.EntityFramework" />
paket add Common.EntityFramework --version 1.0.3
#r "nuget: Common.EntityFramework, 1.0.3"
#:package Common.EntityFramework@1.0.3
#addin nuget:?package=Common.EntityFramework&version=1.0.3
#tool nuget:?package=Common.EntityFramework&version=1.0.3
Common.EntityFramework
A generic Entity Framework repository and unit of work library with built-in pagination, soft deletes, and audit logging.
Packages
| Package | Description |
|---|---|
Common.EntityFramework.Contracts |
Models and interfaces — no EF dependency |
Common.EntityFramework |
Repository implementation — requires EF Core |
Consumers that only need the contracts (DTOs, interfaces) can reference Common.EntityFramework.Contracts alone.
Getting Started
1. Inherit from a base entity
public class Product : EntityBase<int>
{
public string Name { get; set; }
public decimal Price { get; set; }
}
// Or with audit timestamps (adds CreateDate and LastUpdated):
public class Order : AuditableEntityBase<int>
{
public string CustomerName { get; set; }
}
2. Register the Unit of Work
Register your DbContext and then UnitOfWork scoped to it. Both should be scoped so they share the same DbContext instance per request.
services.AddDbContext<AppDbContext>(options => ...);
services.AddScoped<IUnitOfWork<AppDbContext>, UnitOfWork<AppDbContext>>();
To use a custom audit log mapper, use the factory overload instead:
services.AddScoped<IAuditLogMapper, MyAuditLogMapper>();
services.AddScoped<IUnitOfWork<AppDbContext>>(sp =>
new UnitOfWork<AppDbContext>(
sp.GetRequiredService<AppDbContext>(),
sp.GetRequiredService<IAuditLogMapper>()
));
3. Inject and use the Unit of Work
The Unit of Work acts as a transaction boundary. You call Create<T>() to get a repository for any entity type, perform all your reads and writes, then call SaveAsync() once to commit everything together.
public class OrderService
{
private readonly IUnitOfWork<AppDbContext> _uow;
public OrderService(IUnitOfWork<AppDbContext> uow) => _uow = uow;
public async Task PlaceOrder(Order order, IEnumerable<Product> products)
{
var orderRepo = _uow.Create<Order>();
var productRepo = _uow.Create<Product>();
await orderRepo.AddAsync(order);
foreach (var product in products)
{
product.SoftDelete(); // mark unavailable
productRepo.Attach(product);
}
// All changes committed in a single transaction:
await _uow.SaveAsync();
}
}
Reads do not require a save — just call Create<T>() and query:
public async Task<Order> GetOrder(int id)
{
var repo = _uow.Create<Order>();
return await repo.FirstOrDefaultAsync(o => o.Id == id);
}
Repository Methods
| Method | Description |
|---|---|
GetManyAsync(predicate, mapper, ...) |
Returns a projected IEnumerable<TMapped> |
GetManyAsync(predicate, ...) |
Returns raw IQueryable<T> for further composition |
FirstOrDefaultAsync(predicate, ...) |
Returns the first match or null |
FirstOrDefaultAsync<R>(predicate, mapper, ...) |
Returns first match projected, with null-handling mode |
CountAsync(predicate, ...) |
Returns count matching predicate |
Any(predicate) |
Returns true if any record matches |
AddAsync(entity) |
Stages a single entity for insert |
AddAsync(entities) |
Stages multiple entities for insert |
Attach(entity) |
Attaches a detached entity for update tracking |
AttachRange(entities) |
Attaches multiple detached entities |
SoftDelete(entity/entities/predicate) |
Sets DeletedUTC, excluded from future queries |
HardDelete(entity/entities/predicate) |
Permanently removes from the database |
GetPage(request, ...) |
Dynamic paged query with search and ordering |
GetPaginatedAsync(predicate, pageSize, ...) |
Returns full result set chunked into pages |
All query methods filter out soft-deleted records by default. Pass allowDeleted: true to include them.
Features
Soft Deletes
All entities inherit DeletedUTC. Setting it excludes the record from all queries unless allowDeleted: true is passed.
repo.SoftDelete(product); // single entity
repo.SoftDelete(p => p.Price == 0); // predicate overload
await repo.GetManyAsync(p => true, allowDeleted: true); // include soft-deleted
Hard delete is also available:
repo.HardDelete(product);
repo.HardDelete(p => p.Price == 0);
NullMappingMode
Controls the behavior of FirstOrDefaultAsync<R> when no record is found:
// Returns null (default):
await repo.FirstOrDefaultAsync(predicate, mapper, nullMappingMode: NullMappingMode.ReturnNull);
// Returns new R() with default values:
await repo.FirstOrDefaultAsync(predicate, mapper, nullMappingMode: NullMappingMode.ReturnEmpty);
// Runs the mapper against a null source:
await repo.FirstOrDefaultAsync(predicate, mapper, nullMappingMode: NullMappingMode.Map);
Pagination
GetPage accepts a SearchRequest that supports dynamic filtering, sorting, and skip/take:
var request = new SearchRequest
{
Skip = 0,
Take = 20,
SearchTerms = new[]
{
new SearchTerm { Field = "Name", Operator = ConditionalOperator.Like, Value = "widget" }
},
Orderings = new[]
{
new Ordering { Field = "Price", SortDirection = SortDirection.Asc }
}
};
Page<Product> page = await repo.GetPage(request);
// page.Data — the records for this page
// page.TotalRecords — total matches before pagination
// With a mapper:
Page<ProductDto> page = await repo.GetPage(request, p => new ProductDto(p));
GetPaginatedAsync returns the entire result set pre-chunked into pages:
PaginatedList<Product> pages = await repo.GetPaginatedAsync(p => p.Price > 0, pageSize: 10);
var firstPage = pages[0];
Audit Logging
Replace SaveAsync() with SaveWithAuditLogsAsync() to automatically record all pending changes before saving:
await _uow.SaveWithAuditLogsAsync(loggedBy: currentUserId, loggedByType: "Employee");
Each AuditLog record captures:
- Table name and change state (
Added,Modified,Deleted(soft)) - Serialized list of property changes (original → current value)
- Stack trace at the time of save
- Optional logged-by user ID and type
The default mapper (DefaultAuditLogMapper) handles all of this automatically. To customize, implement IAuditLogMapper and register it as shown in step 2 above.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. 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. |
-
net8.0
- Common.EntityFramework.Contracts (>= 1.0.3)
- Microsoft.EntityFrameworkCore (>= 8.0.10)
- System.Linq.Dynamic.Core (>= 1.6.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.