Muslim.GenericRepository 8.0.0

dotnet add package Muslim.GenericRepository --version 8.0.0
                    
NuGet\Install-Package Muslim.GenericRepository -Version 8.0.0
                    
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="Muslim.GenericRepository" Version="8.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Muslim.GenericRepository" Version="8.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Muslim.GenericRepository" />
                    
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 Muslim.GenericRepository --version 8.0.0
                    
#r "nuget: Muslim.GenericRepository, 8.0.0"
                    
#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 Muslim.GenericRepository@8.0.0
                    
#: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=Muslim.GenericRepository&version=8.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Muslim.GenericRepository&version=8.0.0
                    
Install as a Cake Tool

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. The DbContext belongs 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 DbContext and a single Save().

License

See LICENSE.txt. Free for personal and commercial use under the stated conditions.

Product 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. 
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
8.0.0 97 9/23/2026
2.3.3 122 8/10/2026
2.2.6 232 1/19/2025
2.2.4 182 1/17/2025
2.2.3 201 1/4/2025
2.2.2 222 1/4/2025
2.2.1 190 1/4/2025
2.2.0 227 10/5/2024

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.