Muslim.GenericRepository
8.0.0
dotnet add package Muslim.GenericRepository --version 8.0.0
NuGet\Install-Package Muslim.GenericRepository -Version 8.0.0
<PackageReference Include="Muslim.GenericRepository" Version="8.0.0" />
<PackageVersion Include="Muslim.GenericRepository" Version="8.0.0" />
<PackageReference Include="Muslim.GenericRepository" />
paket add Muslim.GenericRepository --version 8.0.0
#r "nuget: Muslim.GenericRepository, 8.0.0"
#:package Muslim.GenericRepository@8.0.0
#addin nuget:?package=Muslim.GenericRepository&version=8.0.0
#tool nuget:?package=Muslim.GenericRepository&version=8.0.0
Muslim.GenericRepository
An EF Core repository for entities that carry audit columns. One generic class gives you reads,
writes, soft delete, bulk operations and raw SQL, with CreatedBy / CreatedDate /
UpdateBy / UpdateDate / DeletedBy / DeletedDate filled in for you on every write.
Implements Muslim.GenericRepository.Interface.
Install this package — the interface comes with it.
dotnet add package Muslim.GenericRepository
Setup
Register your DbContext as the default the repositories resolve:
BaseRepository.SetDbContext<AppDbContext>();
Then derive one interface per entity and let DI resolve it:
public interface IUserRepository : IRepository<User> { }
internal class UserRepository : Repository<User>, IUserRepository { }
Both namespaces are needed, because IRepository<T> sits apart from the interfaces it inherits:
global using Muslim.GenericRepository.Interface; // IRepository<T>
global using Muslim.GenericRepository.Repositories; // Repository<T>, BaseRepository
global using Muslim.IGenericRepository; // ICreate<T>, IFind<T>, …
A repository that must talk to a different context calls UseContext in its constructor. This
affects that instance only:
internal class TenantRepository : Repository<Tenant>, ITenantRepository
{
public TenantRepository() { UseContext<TenantDbContext>(); }
}
Reading
var user = await repo.GetByIdAsync(5); // tracked
var dto = await repo.GetByIdAsync<UserDto>(5); // projected, columns the DTO needs only
var all = await repo.GetAllAsync<UserListDto>();
var some = await repo.FindAsync(x => x.BranchId == id);
var one = await repo.SingleOrDefaultAsync(x => x.Email == email);
Tracking
Every entity-returning read takes isTracking, defaulting to true. Pass false for read-only
queries — EF skips the change tracker, which is measurably cheaper on large result sets:
var readOnly = await repo.GetAllAsync(x => x.IsActive, isTracking: false);
Projection overloads (GetAll<TMap>, Find<TMap>, …) have no such switch: EF never tracks a
projection to a non-entity type, so there would be nothing to turn off.
For anything the named methods do not cover, drop to IQueryable:
var page = await repo.Query
.Where(x => x.IsActive)
.OrderBy(x => x.Name)
.Skip(20).Take(20)
.ToListAsync();
var names = await repo.QueryAsNoTracking.Select(x => x.Name).ToListAsync();
Filtering vs projecting
Two overloads look alike and behave differently:
repo.Find<UserDto>(x => x.IsActive); // filter on the entity, then project ← usually this
repo.Find<UserDto>(d => d.FullName == n); // project, then filter on the DTO
Filtering first is the cheaper plan. Use the second only for predicates over members that exist only after mapping.
Writing
Create / Update / Delete / SoftDelete stage the change on the context. SaveCreate*,
SaveUpdate*, SaveDelete*, SaveSoftDelete* stage and commit.
Use the staging form when several repositories must succeed or fail together:
repo.Create(user);
orderRepo.Update(order);
repo.Save(); // both committed together
Use the Save* form when the write is the whole operation:
var saved = await repo.SaveCreateAsync(user);
var id = saved.Id; // generated key available here
Any of them can map in and out in one call:
var response = await repo.SaveCreateAsync<UserDto, CreateUserDto>(request);
await repo.SaveUpdateAsync(updateUserDto);
Audit columns
Set automatically; do not assign them yourself.
| Operation | Written |
|---|---|
| Create | CreatedDate (UTC), CreatedBy |
| Update | UpdateDate (UTC), UpdateBy — Created* and soft-delete columns are re-read from the database and preserved |
| Soft delete | IsDeleted, DeletedDate (UTC), DeletedBy |
All timestamps are UTC. Convert to local time for display, never for storage or comparison.
Because updates re-read the audit columns from the row, it is safe to update from a DTO that never carried them — mapping would otherwise blank them out. Range updates fetch the whole batch in one query, so updating 100 rows costs one extra round trip, not 100.
Excluding a column from an update
repo.IgnoreUpdateProperty(user, x => x.PasswordHash);
repo.Save(); // every column except PasswordHash is written
Delete and soft delete
IDelete removes the row. ISoftDelete keeps it and sets the flag — use it wherever history
matters. Soft-deleted rows are still returned by queries unless a global query filter excludes
them; the repository writes the flag, it does not hide anything by itself.
Both come in three shapes: returning the entity, returning a mapped DTO (<TMap>), or returning
nothing (*NoReturn, which also skips the mapping).
Bulk operations
These run one SQL statement and do not load entities, so no Save() is needed:
await repo.DeleteWhereAsync(x => x.BranchId == branchId);
await repo.SoftDeleteByIdsAsync(ids);
await repo.RestoreByIdAsync(5); // undo a soft delete
They return the number of rows affected. Because nothing is materialised, EF's change tracker,
interceptors and any SaveChanges override do not see these rows — use them for cleanup of many
rows, and the per-entity methods when the entity's own lifecycle matters.
Raw SQL
Prefer the interpolated members. They read like string interpolation, but every hole becomes a SQL parameter:
var rows = repo.FromSqlInterpolated($"SELECT * FROM Users WHERE Age > {minAge}");
var n = await repo.ExecuteSqlInterpolatedAsync($"UPDATE Users SET IsActive = 0 WHERE Id = {id}");
ExecuteSqlRaw hands your string to the provider untouched. Build it from constants only and pass
caller-supplied values as parameters.
Change tracker
To attach a fresh copy of a row EF is already tracking — the cure for "the instance of entity type 'X' cannot be tracked because another instance with the same key is already being tracked":
repo.DetachLocal(incoming);
repo.Update(incoming);
Notes
Dispose()is obsolete. TheDbContextbelongs to the DI scope and is shared with every other repository in it, so disposing it mid-request breaks the rest of the request. Let the scope do it.- Async methods do not yet accept a
CancellationToken. - There is no transaction API; a multi-repository write relies on the shared
DbContextand a singleSave().
License
See LICENSE.txt. Free for personal and commercial use under the stated conditions.
| 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
- Microsoft.EntityFrameworkCore (>= 8.0.31)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.31)
- Muslim.EntityAndDto (>= 8.0.0)
- Muslim.Extensions (>= 8.0.0)
- Muslim.GenericRepository.Interface (>= 8.0.0)
- Muslim.Helpers (>= 8.0.0)
- Muslim.Inject (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
BaseRepository now resolves every ambient dependency (DbContext, IServiceProvider, IMapper, ICurrentUser) lazily on first use instead of eagerly in the constructor. A repository built before its tenant scope is established (e.g. during login) previously captured IServiceProvider/IMapper/ICurrentUser from that early scope while its DbContext correctly waited for the real one — two halves of the same object reading two different scopes under database-per-tenant. All four now defer the same way, and use InjectRequired rather than Inject: a missing registration for any of them now throws naming the type at first use instead of returning null and failing later as an unrelated NullReferenceException. Source-compatible — no public member changed shape; behavior differs only when a required service was never registered.