Muslim.GenericRepository 8.0.0

There is a newer version of this package available.
See the version list below for details.
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 (1)

Showing the top 1 NuGet packages that depend on Muslim.GenericRepository:

Package Downloads
Muslim.LogInformation.Application

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.1 121 10/15/2026
8.0.0 166 9/23/2026
2.3.3 136 8/10/2026
2.2.6 245 1/19/2025
2.2.4 197 1/17/2025
2.2.3 216 1/4/2025
2.2.2 236 1/4/2025
2.2.1 202 1/4/2025
2.2.0 241 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.