Quick Start
This guide takes you from a blank .NET 10 web app to a working repository pattern in five minutes. We’ll use SQL Server as the example, but every step is identical for PostgreSQL, Oracle, and MySQL — just swap the provider method name.
Install packages
dotnet add package Forge.Repository
dotnet add package Forge.Repository.SqlServerCreate your DbContext
Create Data/AppDbContext.cs. Your context must inherit from Forge.Repository.DbContext, not Microsoft.EntityFrameworkCore.DbContext directly.
using Forge.Repository;
using Microsoft.EntityFrameworkCore;
namespace MyApp.Data;
public class AppDbContext(DbContextOptions<AppDbContext> options)
: DbContext(options)
{
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
}Forge.Repository’s DbContext base class hooks into EF Core’s save pipeline to automatically call OnCreate(), OnUpdate(), and OnDelete() on entities that implement IAuditableBaseModel. You get audit timestamps for free without any extra code.
Define your models
Entities must implement IBaseModel at a minimum. Use IAuditableBaseModel for a full audit trail and soft-delete support. Alternatively, you can inherit from Forge.Models.BaseModel or Forge.Models.AuditableBaseModel.
You can also create your own base class that inherits from these models for reuse across your application (recommended).
using Forge.Interfaces;
namespace MyApp.Entities;
// Minimal entity — just an Id
public class Tag : IBaseModel
{
public string Id { get; set; } = Ulid.NewUlid().ToString();
public string Name { get; set; } = string.Empty;
}
// Auditable entity — full audit trail
public class Product : AuditableBaseModel
{
public override required string Id { get; set; } = Ulid.NewUlid().ToString();
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string CategoryId { get; set; } = string.Empty;
public override void OnCreate()
{
base.OnCreate();
//Get the user id in whatever way is convenient for you.
CreatedBy = USER_ID;
}
public override void OnUpdate()
{
base.OnUpdate();
//Get the user id in whatever way is convenient for you.
ModifiedBy = USER_ID;
}
public override void OnDelete()
{
base.OnDelete();
//Any custom logic you want to execute when an entity is deleted.
}
}Register in Program.cs
using Forge.DbTuner;
using Forge.SqlServer;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddForgeRepositorySqlServer<AppDbContext>(
connectionString: builder.Configuration.GetConnectionString("Default")!,
poolingOptions: new ForgeDbContextPoolingOptions
{
EnablePooling = true,
MinPoolSize = 1,
MaxPoolSize = 20,
},
configure: tuner =>
{
tuner.SetRetry(3); // retry transient failures up to 3 times
tuner.SetTimeout(30); // command timeout of 30 seconds
tuner.EnableLazyLoading(); // enable lazy loading of navigation properties
});
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();Use in a service
Inject IUnitOfWork - it gives you access to typed repositories for all your entities.
using Forge.Interfaces;
namespace MyApp.Services;
public class ProductService(IUnitOfWork uow) : IProductService
{
// Resolve a typed repository from the unit of work
private readonly IRepositoryAsync<Product> _products =
uow.GetRepositoryAsync<Product>();
// Get all (non-deleted) products
public async Task<IList<Product>> GetAllAsync(CancellationToken ct)
=> await _products.FindAllAsync(p => !p.IsDeleted, ct);
// Get a single product by Id
public async Task<Product?> GetByIdAsync(string id, CancellationToken ct)
=> await _products.GetByIdAsync(id, ct);
// Create a product — SaveChangesAsync commits to the database
public async Task CreateAsync(Product product, CancellationToken ct)
{
await _products.AddAsync(product, ct);
await uow.SaveChangesAsync(ct);
}
// Update
public async Task UpdateAsync(Product product, CancellationToken ct)
{
await _products.UpdateAsync(product, ct);
await uow.SaveChangesAsync(ct);
}
// Soft delete — sets IsDeleted = true via OnDelete()
public async Task DeleteAsync(Product product, CancellationToken ct)
{
await _products.RemoveAsync(product, ct);
await uow.SaveChangesAsync(ct);
}
}Wire up the controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController(IProductService svc) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAll(CancellationToken ct)
=> Ok(await svc.GetAllAsync(ct));
[HttpGet("{id}")]
public async Task<IActionResult> Get(string id, CancellationToken ct)
{
var product = await svc.GetByIdAsync(id, ct);
return product is null ? NotFound() : Ok(product);
}
[HttpPost]
public async Task<IActionResult> Create(Product product, CancellationToken ct)
{
await svc.CreateAsync(product, ct);
return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}
}What Just Happened?
When you called AddForgeRepositorySqlServer<AppDbContext>():
- Your
AppDbContextwas registered with EF Core (pooled or non-pooled based onForgeDbContextPoolingOptions) IUnitOfWorkwas registered as scoped →DBUnitOfWorkIRepositoryAsync<>was registered as scoped →DBRepository<>- Connection string pooling was applied at the
SqlConnectionStringBuilderlevel - Retry policy and timeout were applied to the EF Core SQL Server options including Lazy Loading
Calling uow.GetRepositoryAsync<Product>() resolves a DBRepository<Product> that is bound to the same AppDbContext instance for the duration of the HTTP request scope - so all changes across multiple repositories are committed together in one SaveChangesAsync call.
Next Steps
- Entities & Models — deep dive into
IBaseModelandIAuditableBaseModel - Repository API — every method on
IRepositoryAsync<T> - Unit of Work — multi-repository transactions
- Criteria Pattern — encapsulate complex queries
- Raw SQL — escape the ORM when you need to