Muslim.GenericRepository
10.0.1
dotnet add package Muslim.GenericRepository --version 10.0.1
NuGet\Install-Package Muslim.GenericRepository -Version 10.0.1
<PackageReference Include="Muslim.GenericRepository" Version="10.0.1" />
<PackageVersion Include="Muslim.GenericRepository" Version="10.0.1" />
<PackageReference Include="Muslim.GenericRepository" />
paket add Muslim.GenericRepository --version 10.0.1
#r "nuget: Muslim.GenericRepository, 10.0.1"
#:package Muslim.GenericRepository@10.0.1
#addin nuget:?package=Muslim.GenericRepository&version=10.0.1
#tool nuget:?package=Muslim.GenericRepository&version=10.0.1
Muslim.GenericRepository
An EF Core repository for .NET 10. One generic class gives every table reads, writes, soft delete,
bulk operations and raw SQL, and fills CreatedBy, UpdateBy, DeletedBy and their dates on every
write.
dotnet add package Muslim.GenericRepository
This brings in Muslim.GenericRepository.Interface with it.
Full documentation: genericrepository.netlify.app
Quick start
An entity, and one repository interface for it:
using Muslim.EntityAndDto.Entitys;
using Muslim.GenericRepository.Interface;
using Muslim.GenericRepository.Repositories;
public class Product : Entity
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
}
public interface IProductRepository : IRepository<Product> { }
internal class ProductRepository : Repository<Product>, IProductRepository { }
Setup in Program.cs:
using Muslim.Inject;
using Muslim.GenericRepository.Repositories;
builder.Services.AddDbContext<ShopContext>(o => o.UseSqlServer(connectionString));
builder.Services.AddAutoMapper(typeof(ProductProfile).Assembly);
builder.Services.AddHttpContextAccessor();
builder.Services.AddAutoInjection(typeof(IProductRepository).Assembly); // repositories and your ICurrentUser
BaseRepository.SetDbContext<ShopContext>();
var app = builder.Build();
InjectExtensions.SetServiceProvider(app.Services);
app.Use(async (context, next) =>
{
InjectExtensions.SetServiceProvider(context.RequestServices);
await next();
});
You also need a class that implements ICurrentUser from
Muslim.Helpers. The repository uses it to fill
CreatedBy, UpdateBy and DeletedBy.
Use the repository:
public class ProductService(IProductRepository products)
{
public Task<ProductDto> GetAsync(int id)
=> products.GetByIdAsync<ProductDto>(id);
public async Task<int> CreateAsync(CreateProductDto dto)
{
var product = await products.SaveCreateAsync(new Product { Name = dto.Name, Price = dto.Price });
return product.Id;
}
public Task DeleteAsync(int id)
=> products.SaveSoftDeleteByIdAsync(id);
}
Setup notes
DTOs used with the repository inherit
Dtofrom Muslim.EntityAndDto, and need an AutoMapper map from the entity.The middleware that sets the provider per request is required. Without it, every request shares one
DbContext, or startup fails with "Cannot resolve scoped service".In background work, wrap each unit of work in
InjectExtensions.RunInScopeAsync.AddAutoInjectionregistersIProductRepositoryas scoped. It does not register open generics. To injectIRepository<T>directly, addbuilder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));IRepository<T>and the interfaces it is built from are in different namespaces. Import both:global using Muslim.GenericRepository.Interface; // IRepository<T> global using Muslim.IGenericRepository; // ICreate<T>, IFind<T>, ... global using Muslim.GenericRepository.Repositories; // Repository<T>, BaseRepositoryA repository that works on another context calls
UseContextin its constructor. This affects that repository only:internal class TenantRepository : Repository<Tenant>, ITenantRepository { public TenantRepository() { UseContext<TenantDbContext>(); } }
Reading
var product = await repo.GetByIdAsync(5); // tracked entity
var dto = await repo.GetByIdAsync<ProductDto>(5); // only the columns the DTO needs
var all = await repo.GetAllAsync<ProductListDto>();
var cheap = await repo.FindAsync(x => x.Price < 10);
var one = await repo.SingleOrDefaultAsync(x => x.Name == name);
var readOnly = await repo.GetAllAsync(x => x.Price > 0, isTracking: false);
Reads that return entities are tracked by default. Pass isTracking: false for read-only queries;
it is faster on large results. Reads that return a DTO are never tracked.
For anything else, use the query directly:
var page = await repo.QueryAsNoTracking
.Where(x => x.Price > 0)
.OrderBy(x => x.Name)
.Skip(20).Take(20)
.ToListAsync();
Filter the entity, or the DTO
Some methods accept a condition on the entity or on the DTO. Write the parameter type, or the call does not compile (CS0121):
repo.FirstOrDefault<ProductDto>((Product x) => x.Price > 0); // filter, then map — use this
repo.FirstOrDefault<ProductDto>((ProductDto d) => d.Label == text); // map, then filter
repo.Find((Product x) => x.Price > 0, x => new { x.Id, x.Name }); // your own selector
Filtering the entity is faster. Filter the DTO only on fields that exist after mapping.
Writing
Create, Update, Delete and SoftDelete add the change without saving. SaveCreate,
SaveUpdate, SaveDelete and SaveSoftDelete add it and save.
Save once when several changes must succeed or fail together:
productRepo.Create(product);
orderRepo.Update(order);
productRepo.Save(); // both saved together
Use the Save form when the write is the whole job:
var saved = await repo.SaveCreateAsync(product);
var id = saved.Id;
Any write can map from a DTO and back in one call:
var response = await repo.SaveCreateAsync<ProductDto, CreateProductDto>(request);
await repo.SaveUpdateAsync(updateProductDto);
Audit columns
These are filled for you. Do not set them yourself.
| Operation | Filled |
|---|---|
| Create | CreatedDate, CreatedBy |
| Update | UpdateDate, UpdateBy — Created* and the soft-delete columns are read back from the database and kept |
| Soft delete | IsDeleted, DeletedDate, DeletedBy |
All dates are UTC. Convert to local time only for display.
Because updates read the audit columns back from the row, updating from a DTO that does not have them is safe. Updating many rows reads them in one query.
Updating a row that is already loaded
var product = await repo.GetByIdAsync(dto.Id) ?? throw new KeyNotFoundException();
await repo.SaveUpdateAsync(dto); // the loaded copy is released, the DTO's values are saved
If another copy of the same row is already tracked, it is released first and its unsaved edits are lost. A pending insert or delete of that row is never dropped; EF Core's "already being tracked" error is raised instead.
Skipping a column on update
repo.IgnoreUpdateProperty(user, x => x.PasswordHash);
repo.Save(); // every column except PasswordHash is saved
Audit columns are handled the same way as in Update. A missing id throws KeyNotFoundException.
Delete and soft delete
Delete removes the row. SoftDelete keeps it and marks it deleted. Use soft delete wherever
history matters.
The repository only sets the flag. Soft-deleted rows still come back in queries unless your context
has a query filter such as HasQueryFilter(x => !x.IsDeleted) —
Muslim.ConfigureTable adds it for you.
With that filter, RestoreByIdAsync and RestoreWhereAsync cannot see deleted rows and return
0. Restore through a repository that uses a context without the filter (UseContext<T>()).
Bulk operations
These run one SQL statement without loading rows, so no Save() is needed:
await repo.DeleteWhereAsync(x => x.CategoryId == categoryId);
await repo.SoftDeleteByIdsAsync(ids);
await repo.RestoreByIdAsync(5);
They return the number of rows changed. Because no rows are loaded, the change tracker and any
SaveChanges override do not see them.
Raw SQL
Use the interpolated methods. Every {value} becomes a SQL parameter:
var rows = repo.FromSqlInterpolated($"SELECT * FROM Product WHERE Price > {minPrice}");
var n = await repo.ExecuteSqlInterpolatedAsync($"UPDATE Product SET Price = 0 WHERE Id = {id}");
ExecuteSqlRaw sends your text as it is. Build it from constants only.
Good to know
- Do not call
Dispose(). TheDbContextbelongs to the request and is shared by every repository in it. - Async methods do not take a
CancellationTokenyet. - There is no transaction API. To save several changes together, add them and call
Save()once. - For tests, use a relational provider such as SQLite in memory. EF Core's InMemory provider does not support the bulk operations.
Related packages
| Package | Use it for |
|---|---|
| Muslim.GenericRepository.Interface | The repository interfaces |
| Muslim.EntityAndDto | Base entity and DTO classes |
| Muslim.Inject | Automatic dependency registration |
| Muslim.Extensions | Mapping, password hashing and small helpers |
| Muslim.Helpers | The current-user contract |
Coding-agent guidance
When you build a project that uses this package, a short guide for AI coding agents is copied to
.agents/skills/muslim-genericrepository/ at the root of your repository. Agents that read skills
from .agents/skills, such as Codex and GitHub Copilot in VS Code, use it to write correct code
against this package. Your own files and other skills are never touched, and nothing is deleted
automatically.
To turn it off, add this to your project or Directory.Build.props:
<PropertyGroup>
<MuslimGenericRepositoryGenerateSkill>false</MuslimGenericRepositoryGenerateSkill>
</PropertyGroup>
License
See LICENSE.txt.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Microsoft.EntityFrameworkCore (>= 10.0.13)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.13)
- Muslim.EntityAndDto (>= 10.0.1)
- Muslim.Extensions (>= 10.0.1)
- Muslim.GenericRepository.Interface (>= 10.0.1)
- Muslim.Helpers (>= 10.0.1)
- Muslim.Inject (>= 10.0.1)
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.
Update/UpdateRange (entity and DTO forms) no longer throw "another instance with the same key value is already being tracked" when the row was loaded earlier in the same context: the stale unchanged/modified copy is detached and the incoming instance attached. IgnoreUpdateProperty now stamps and preserves audit columns like every other update instead of writing null CreatedBy/CreatedDate. Ships version-matched agent guidance and generates .agents/skills/muslim-genericrepository during normal consuming builds; preserves user files and supports MuslimGenericRepositoryGenerateSkill=false. See CHANGELOG.md and README.md.