Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Build .NET backend services with clean architecture, dependency injection, Entity Framework, and ASP.NET Core patterns.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
assets/repository-template.cs
1// Repository Implementation Template for .NET 8+2// Demonstrates both Dapper (performance) and EF Core (convenience) patterns34using System.Data;5using Dapper;6using Microsoft.Data.SqlClient;7using Microsoft.EntityFrameworkCore;8using Microsoft.Extensions.Logging;910namespace YourNamespace.Infrastructure.Data;1112#region Interfaces1314public interface IProductRepository15{16Task<Product?> GetByIdAsync(string id, CancellationToken ct = default);17Task<Product?> GetBySkuAsync(string sku, CancellationToken ct = default);18Task<(IReadOnlyList<Product> Items, int TotalCount)> SearchAsync(ProductSearchRequest request, CancellationToken ct = default);19Task<Product> CreateAsync(Product product, CancellationToken ct = default);20Task<Product> UpdateAsync(Product product, CancellationToken ct = default);21Task DeleteAsync(string id, CancellationToken ct = default);22Task<IReadOnlyList<Product>> GetByIdsAsync(IEnumerable<string> ids, CancellationToken ct = default);23}2425#endregion2627#region Dapper Implementation (High Performance)2829public class DapperProductRepository : IProductRepository30{31private readonly IDbConnection _connection;32private readonly ILogger<DapperProductRepository> _logger;3334public DapperProductRepository(35IDbConnection connection,36ILogger<DapperProductRepository> logger)37{38_connection = connection;39_logger = logger;40}4142public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)43{44const string sql = """45SELECT Id, Name, Sku, Price, CategoryId, Stock, CreatedAt, UpdatedAt46FROM Products47WHERE Id = @Id AND IsDeleted = 048""";4950return await _connection.QueryFirstOrDefaultAsync<Product>(51new CommandDefinition(sql, new { Id = id }, cancellationToken: ct));52}5354public async Task<Product?> GetBySkuAsync(string sku, CancellationToken ct = default)55{56const string sql = """57SELECT Id, Name, Sku, Price, CategoryId, Stock, CreatedAt, UpdatedAt58FROM Products59WHERE Sku = @Sku AND IsDeleted = 060""";6162return await _connection.QueryFirstOrDefaultAsync<Product>(63new CommandDefinition(sql, new { Sku = sku }, cancellationToken: ct));64}6566public async Task<(IReadOnlyList<Product> Items, int TotalCount)> SearchAsync(67ProductSearchRequest request,68CancellationToken ct = default)69{70var whereClauses = new List<string> { "IsDeleted = 0" };71var parameters = new DynamicParameters();7273// Build dynamic WHERE clause74if (!string.IsNullOrWhiteSpace(request.SearchTerm))75{76whereClauses.Add("(Name LIKE @SearchTerm OR Sku LIKE @SearchTerm)");77parameters.Add("SearchTerm", $"%{request.SearchTerm}%");78}7980if (request.CategoryId.HasValue)81{82whereClauses.Add("CategoryId = @CategoryId");83parameters.Add("CategoryId", request.CategoryId.Value);84}8586if (request.MinPrice.HasValue)87{88whereClauses.Add("Price >= @MinPrice");89parameters.Add("MinPrice", request.MinPrice.Value);90}9192if (request.MaxPrice.HasValue)93{94whereClauses.Add("Price <= @MaxPrice");95parameters.Add("MaxPrice", request.MaxPrice.Value);96}9798var whereClause = string.Join(" AND ", whereClauses);99var page = request.Page ?? 1;100var pageSize = request.PageSize ?? 50;101var offset = (page - 1) * pageSize;102103parameters.Add("Offset", offset);104parameters.Add("PageSize", pageSize);105106// Use multi-query for count + data in single roundtrip107var sql = $"""108-- Count query109SELECT COUNT(*) FROM Products WHERE {whereClause};110111-- Data query with pagination112SELECT Id, Name, Sku, Price, CategoryId, Stock, CreatedAt, UpdatedAt113FROM Products114WHERE {whereClause}115ORDER BY Name116OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY;117""";118119using var multi = await _connection.QueryMultipleAsync(120new CommandDefinition(sql, parameters, cancellationToken: ct));121122var totalCount = await multi.ReadSingleAsync<int>();123var items = (await multi.ReadAsync<Product>()).ToList();124125return (items, totalCount);126}127128public async Task<Product> CreateAsync(Product product, CancellationToken ct = default)129{130const string sql = """131INSERT INTO Products (Id, Name, Sku, Price, CategoryId, Stock, CreatedAt, IsDeleted)132VALUES (@Id, @Name, @Sku, @Price, @CategoryId, @Stock, @CreatedAt, 0);133134SELECT Id, Name, Sku, Price, CategoryId, Stock, CreatedAt, UpdatedAt135FROM Products WHERE Id = @Id;136""";137138return await _connection.QuerySingleAsync<Product>(139new CommandDefinition(sql, product, cancellationToken: ct));140}141142public async Task<Product> UpdateAsync(Product product, CancellationToken ct = default)143{144const string sql = """145UPDATE Products146SET Name = @Name,147Sku = @Sku,148Price = @Price,149CategoryId = @CategoryId,150Stock = @Stock,151UpdatedAt = @UpdatedAt152WHERE Id = @Id AND IsDeleted = 0;153154SELECT Id, Name, Sku, Price, CategoryId, Stock, CreatedAt, UpdatedAt155FROM Products WHERE Id = @Id;156""";157158return await _connection.QuerySingleAsync<Product>(159new CommandDefinition(sql, product, cancellationToken: ct));160}161162public async Task DeleteAsync(string id, CancellationToken ct = default)163{164const string sql = """165UPDATE Products166SET IsDeleted = 1, UpdatedAt = @UpdatedAt167WHERE Id = @Id168""";169170await _connection.ExecuteAsync(171new CommandDefinition(sql, new { Id = id, UpdatedAt = DateTime.UtcNow }, cancellationToken: ct));172}173174public async Task<IReadOnlyList<Product>> GetByIdsAsync(175IEnumerable<string> ids,176CancellationToken ct = default)177{178var idList = ids.ToList();179if (idList.Count == 0)180return Array.Empty<Product>();181182const string sql = """183SELECT Id, Name, Sku, Price, CategoryId, Stock, CreatedAt, UpdatedAt184FROM Products185WHERE Id IN @Ids AND IsDeleted = 0186""";187188var results = await _connection.QueryAsync<Product>(189new CommandDefinition(sql, new { Ids = idList }, cancellationToken: ct));190191return results.ToList();192}193}194195#endregion196197#region EF Core Implementation (Rich Domain Models)198199public class EfCoreProductRepository : IProductRepository200{201private readonly AppDbContext _context;202private readonly ILogger<EfCoreProductRepository> _logger;203204public EfCoreProductRepository(205AppDbContext context,206ILogger<EfCoreProductRepository> logger)207{208_context = context;209_logger = logger;210}211212public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)213{214return await _context.Products215.AsNoTracking()216.FirstOrDefaultAsync(p => p.Id == id, ct);217}218219public async Task<Product?> GetBySkuAsync(string sku, CancellationToken ct = default)220{221return await _context.Products222.AsNoTracking()223.FirstOrDefaultAsync(p => p.Sku == sku, ct);224}225226public async Task<(IReadOnlyList<Product> Items, int TotalCount)> SearchAsync(227ProductSearchRequest request,228CancellationToken ct = default)229{230var query = _context.Products.AsNoTracking();231232// Apply filters233if (!string.IsNullOrWhiteSpace(request.SearchTerm))234{235var term = request.SearchTerm.ToLower();236query = query.Where(p =>237p.Name.ToLower().Contains(term) ||238p.Sku.ToLower().Contains(term));239}240241if (request.CategoryId.HasValue)242query = query.Where(p => p.CategoryId == request.CategoryId.Value);243244if (request.MinPrice.HasValue)245query = query.Where(p => p.Price >= request.MinPrice.Value);246247if (request.MaxPrice.HasValue)248query = query.Where(p => p.Price <= request.MaxPrice.Value);249250// Get count before pagination251var totalCount = await query.CountAsync(ct);252253// Apply pagination254var page = request.Page ?? 1;255var pageSize = request.PageSize ?? 50;256257var items = await query258.OrderBy(p => p.Name)259.Skip((page - 1) * pageSize)260.Take(pageSize)261.ToListAsync(ct);262263return (items, totalCount);264}265266public async Task<Product> CreateAsync(Product product, CancellationToken ct = default)267{268_context.Products.Add(product);269await _context.SaveChangesAsync(ct);270return product;271}272273public async Task<Product> UpdateAsync(Product product, CancellationToken ct = default)274{275_context.Products.Update(product);276await _context.SaveChangesAsync(ct);277return product;278}279280public async Task DeleteAsync(string id, CancellationToken ct = default)281{282var product = await _context.Products.FindAsync(new object[] { id }, ct);283if (product != null)284{285product.IsDeleted = true;286product.UpdatedAt = DateTime.UtcNow;287await _context.SaveChangesAsync(ct);288}289}290291public async Task<IReadOnlyList<Product>> GetByIdsAsync(292IEnumerable<string> ids,293CancellationToken ct = default)294{295var idList = ids.ToList();296if (idList.Count == 0)297return Array.Empty<Product>();298299return await _context.Products300.AsNoTracking()301.Where(p => idList.Contains(p.Id))302.ToListAsync(ct);303}304}305306#endregion307308#region DbContext Configuration309310public class AppDbContext : DbContext311{312public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }313314public DbSet<Product> Products => Set<Product>();315public DbSet<Category> Categories => Set<Category>();316public DbSet<Order> Orders => Set<Order>();317public DbSet<OrderItem> OrderItems => Set<OrderItem>();318319protected override void OnModelCreating(ModelBuilder modelBuilder)320{321// Apply all configurations from assembly322modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);323324// Global query filter for soft delete325modelBuilder.Entity<Product>().HasQueryFilter(p => !p.IsDeleted);326}327}328329public class ProductConfiguration : IEntityTypeConfiguration<Product>330{331public void Configure(EntityTypeBuilder<Product> builder)332{333builder.ToTable("Products");334335builder.HasKey(p => p.Id);336builder.Property(p => p.Id).HasMaxLength(40);337338builder.Property(p => p.Name)339.HasMaxLength(200)340.IsRequired();341342builder.Property(p => p.Sku)343.HasMaxLength(50)344.IsRequired();345346builder.Property(p => p.Price)347.HasPrecision(18, 2);348349// Indexes350builder.HasIndex(p => p.Sku).IsUnique();351builder.HasIndex(p => p.CategoryId);352builder.HasIndex(p => new { p.CategoryId, p.Name });353354// Relationships355builder.HasOne(p => p.Category)356.WithMany(c => c.Products)357.HasForeignKey(p => p.CategoryId);358}359}360361#endregion362363#region Advanced Patterns364365/// <summary>366/// Unit of Work pattern for coordinating multiple repositories367/// </summary>368public interface IUnitOfWork : IDisposable369{370IProductRepository Products { get; }371IOrderRepository Orders { get; }372Task<int> SaveChangesAsync(CancellationToken ct = default);373Task BeginTransactionAsync(CancellationToken ct = default);374Task CommitAsync(CancellationToken ct = default);375Task RollbackAsync(CancellationToken ct = default);376}377378public class UnitOfWork : IUnitOfWork379{380private readonly AppDbContext _context;381private IDbContextTransaction? _transaction;382383public IProductRepository Products { get; }384public IOrderRepository Orders { get; }385386public UnitOfWork(387AppDbContext context,388IProductRepository products,389IOrderRepository orders)390{391_context = context;392Products = products;393Orders = orders;394}395396public async Task<int> SaveChangesAsync(CancellationToken ct = default)397=> await _context.SaveChangesAsync(ct);398399public async Task BeginTransactionAsync(CancellationToken ct = default)400{401_transaction = await _context.Database.BeginTransactionAsync(ct);402}403404public async Task CommitAsync(CancellationToken ct = default)405{406if (_transaction != null)407{408await _transaction.CommitAsync(ct);409await _transaction.DisposeAsync();410_transaction = null;411}412}413414public async Task RollbackAsync(CancellationToken ct = default)415{416if (_transaction != null)417{418await _transaction.RollbackAsync(ct);419await _transaction.DisposeAsync();420_transaction = null;421}422}423424public void Dispose()425{426_transaction?.Dispose();427_context.Dispose();428}429}430431/// <summary>432/// Specification pattern for complex queries433/// </summary>434public interface ISpecification<T>435{436Expression<Func<T, bool>> Criteria { get; }437List<Expression<Func<T, object>>> Includes { get; }438List<string> IncludeStrings { get; }439Expression<Func<T, object>>? OrderBy { get; }440Expression<Func<T, object>>? OrderByDescending { get; }441int? Take { get; }442int? Skip { get; }443}444445public abstract class BaseSpecification<T> : ISpecification<T>446{447public Expression<Func<T, bool>> Criteria { get; private set; } = _ => true;448public List<Expression<Func<T, object>>> Includes { get; } = new();449public List<string> IncludeStrings { get; } = new();450public Expression<Func<T, object>>? OrderBy { get; private set; }451public Expression<Func<T, object>>? OrderByDescending { get; private set; }452public int? Take { get; private set; }453public int? Skip { get; private set; }454455protected void AddCriteria(Expression<Func<T, bool>> criteria) => Criteria = criteria;456protected void AddInclude(Expression<Func<T, object>> include) => Includes.Add(include);457protected void AddInclude(string include) => IncludeStrings.Add(include);458protected void ApplyOrderBy(Expression<Func<T, object>> orderBy) => OrderBy = orderBy;459protected void ApplyOrderByDescending(Expression<Func<T, object>> orderBy) => OrderByDescending = orderBy;460protected void ApplyPaging(int skip, int take) { Skip = skip; Take = take; }461}462463// Example specification464public class ProductsByCategorySpec : BaseSpecification<Product>465{466public ProductsByCategorySpec(int categoryId, int page, int pageSize)467{468AddCriteria(p => p.CategoryId == categoryId);469AddInclude(p => p.Category);470ApplyOrderBy(p => p.Name);471ApplyPaging((page - 1) * pageSize, pageSize);472}473}474475#endregion476477#region Entity Definitions478479public class Product480{481public string Id { get; set; } = string.Empty;482public string Name { get; set; } = string.Empty;483public string Sku { get; set; } = string.Empty;484public decimal Price { get; set; }485public int CategoryId { get; set; }486public int Stock { get; set; }487public bool IsDeleted { get; set; }488public DateTime CreatedAt { get; set; }489public DateTime? UpdatedAt { get; set; }490491// Navigation492public Category? Category { get; set; }493}494495public class Category496{497public int Id { get; set; }498public string Name { get; set; } = string.Empty;499public ICollection<Product> Products { get; set; } = new List<Product>();500}501502public class Order503{504public int Id { get; set; }505public string CustomerOrderCode { get; set; } = string.Empty;506public decimal Total { get; set; }507public DateTime CreatedAt { get; set; }508public ICollection<OrderItem> Items { get; set; } = new List<OrderItem>();509}510511public class OrderItem512{513public int Id { get; set; }514public int OrderId { get; set; }515public string ProductId { get; set; } = string.Empty;516public int Quantity { get; set; }517public decimal UnitPrice { get; set; }518519public Order? Order { get; set; }520public Product? Product { get; set; }521}522523#endregion524