Muslim.GenericRepository 10.0.1

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

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 Dto from 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.

  • AddAutoInjection registers IProductRepository as scoped. It does not register open generics. To inject IRepository<T> directly, add builder.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>, BaseRepository
    
  • A repository that works on another context calls UseContext in 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(). The DbContext belongs to the request and is shared by every repository in it.
  • Async methods do not take a CancellationToken yet.
  • 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.
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 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. 
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

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.