Ledbim.EntityFramework
1.2.0
dotnet add package Ledbim.EntityFramework --version 1.2.0
NuGet\Install-Package Ledbim.EntityFramework -Version 1.2.0
<PackageReference Include="Ledbim.EntityFramework" Version="1.2.0" />
<PackageVersion Include="Ledbim.EntityFramework" Version="1.2.0" />
<PackageReference Include="Ledbim.EntityFramework" />
paket add Ledbim.EntityFramework --version 1.2.0
#r "nuget: Ledbim.EntityFramework, 1.2.0"
#:package Ledbim.EntityFramework@1.2.0
#addin nuget:?package=Ledbim.EntityFramework&version=1.2.0
#tool nuget:?package=Ledbim.EntityFramework&version=1.2.0
Ledbim.EntityFramework
Versiyon: 1.2.0 | Target: .NET 10 | Bağımlı:
Ledbim.Core, EF Core (SQL Server), Dapper
Entity Framework Core ve Dapper tabanlı veri erişim altyapısı. BaseContext ile audit trail ve soft delete otomasyonunu, Repository<T> ile change-tracking tabanlı CRUD'u, GenericExpression<T> ile compile-time güvenli dinamik sorgu oluşturmayı ve DapperExecutor ile EF Core transaction'ını paylaşan ham SQL çalıştırmayı tek pakette sunar.
İçindekiler
- Paket Amacı
- Paket Kapsamı
- Bağımlılıklar
- Önemli Yapılar
- BaseContext
- IRepository<T> ve Repository<T>
- GenericExpression<T>
- PagedFilterRequest<T>
- FilterQueryHelper ve SortQueryHelper
- Dapper Entegrasyonu
- Entegrasyon ve DI Kaydı
- Proje Bazlı Kurulum (IUnitOfWork)
- Kullanım Örnekleri
- Mimari Notlar
- Dikkat Edilmesi Gerekenler
- Hızlı Başlangıç
1. Paket Amacı
Bu paket ne işe yarar?
Projenin veri erişim katmanının ihtiyaç duyduğu tekrar eden altyapı kodunu ortadan kaldırır:
- Audit trail:
CreatedDate,CreatedBy,UpdatedDate,UpdatedBy,DeletedDate,DeletedByalanlarıSaveChangesAsyncsırasında otomatik doldurulur. - Soft delete:
Remove()çağrılarıIHardDeleteuygulamayan entity'leri fiziksel silmek yerineIsDeleted = trueolarak işaretler. - Change-tracking repository: Handler'lar
SaveChangesçağırmaz; bu sorumluluk pipeline'dakiTransactionBehaviour'a aittir. - Bulk operasyonlar:
ExecuteUpdateAsyncveExecuteDeleteAsync, change tracker'ı atlayarak doğrudan SQL yürütür ve audit alanlarını otomatik yönetir. - Dinamik sorgular:
GenericExpression<T>ile predicate, include, orderBy ve global query filter bypass tek bir nesnede taşınır. - Gelişmiş filtreleme:
FilterQueryHelper, client'tan gelen JSON filtre yapısını — nested gruplar, dot-path navigasyon ve koleksiyon operasyonları dahil —Expression<Func<T, bool>>'e dönüştürür. - Dapper paylaşımı:
DapperExecutor, EF Core'un aktif connection ve transaction'ını paylaşır; iki teknoloji aynı transaction içinde birlikte kullanılabilir.
Ne zaman kullanılmalıdır?
SQL Server tabanlı her .NET projesinde, veri erişim katmanı (Infrastructure veya Persistence) Ledbim.Core sözleşmelerini implement edecekse.
2. Paket Kapsamı
Bu pakette neler var?
| Alan | İçerik |
|---|---|
| DbContext | BaseContext — audit, soft delete, domain event toplama |
| Repository | IRepository<T>, Repository<T>, RepositoryExtensions |
| Sorgu Oluşturma | GenericExpression<T>, PagedFilterRequest<T> |
| Filtre/Sort | FilterQueryHelper, SortQueryHelper |
| Dapper | IDapperExecutor, DapperExecutor, IDbConnectionProvider, EfCoreDbConnectionProvider<T> |
| DI Uzantısı | AddRepositories<TContext>() |
Neler bu paket dışında tutulmuştur?
| Alan | Nerede bulunur? |
|---|---|
Proje-özgü IUnitOfWork ve implementasyonu |
Projenin Infrastructure katmanı |
| Proje-özgü repository interface'leri | Projenin Application ve Infrastructure katmanları |
| EF Core migration'ları | Projenin Infrastructure katmanı |
DbContext OnModelCreating, global query filter tanımları |
Projenin Infrastructure katmanı |
| MongoDB repository | Ledbim.Mongo |
3. Bağımlılıklar
NuGet Bağımlılıkları
| Paket | Versiyon | Kullanım Amacı |
|---|---|---|
Microsoft.EntityFrameworkCore.SqlServer |
10.0.1 | DbContext, IQueryable, change tracking |
Dapper |
2.1.66 | Ham SQL sorgu desteği |
Ledbim Bağımlılıkları
| Paket | Kullanım Amacı |
|---|---|
Ledbim.Core |
BaseEntity, ICurrentUserAccessor, IDomainEventCollector, IAggregateRoot, IHardDelete, PagedModel<T>, PaginationFilterQuery |
4. Önemli Yapılar
Public Interface'ler
| Interface | Sorumluluk |
|---|---|
IRepository<T> |
Generic veri erişim sözleşmesi (write + bulk + read) |
IDapperExecutor |
Ham SQL sorgu yürütme (QueryAsync, QuerySingleOrDefaultAsync, ExecuteAsync) |
IDbConnectionProvider |
Aktif DbConnection ve DbTransaction'a erişim |
Public Class'lar
| Sınıf | Sorumluluk |
|---|---|
BaseContext |
Audit trail, soft delete ve domain event toplama entegre DbContext |
Repository<T> |
IRepository<T> implementasyonu |
DapperExecutor |
IDapperExecutor implementasyonu; EF Core'un connection/transaction'ını paylaşır |
EfCoreDbConnectionProvider<TContext> |
EF Core'un underlying DbConnection ve DbTransaction'ını expose eder |
GenericExpression<T> |
Predicate + include + orderBy + query filter bypass taşıyıcısı |
PagedFilterRequest<T> |
Sayfalama, filtreleme ve sıralama isteği |
5. BaseContext
Constructor
public abstract class BaseContext(
DbContextOptions options,
ICurrentUserAccessor currentUserAccessor,
IDomainEventCollector? domainEventCollector = null) : DbContext(options)
IDomainEventCollector nullable'dır. Domain event kullanmayan projeler bu parametreyi geçmeyebilir.
SaveChangesAsync Davranışı
SaveChangesAsync() çağrıldığında şu adımlar sırayla çalışır:
1. UpdateAuditEntities()
├─ EntityState.Added → CreatedDate = UtcNow, CreatedBy = currentUser
├─ EntityState.Modified → UpdatedDate = UtcNow, UpdatedBy = currentUser
└─ EntityState.Deleted → (soft delete davranışı — aşağıya bakın)
2. CollectDomainEvents()
├─ ChangeTracker'daki IAggregateRoot'ları tarar
├─ Her aggregate'in DomainEvents listesini IDomainEventCollector'a aktarır
└─ Aggregate'in event listesi temizlenir
3. base.SaveChangesAsync()
└─ SQL yazılır, DB'ye persist edilir
Soft Delete Davranışı
Repository.Remove(entity) → EntityState.Deleted
BaseContext.UpdateAuditEntities() bunu yakalar:
IHardDelete değilse:
→ EntityState = Modified
→ IsDeleted = true
→ DeletedDate = UtcNow
→ DeletedBy = currentUser
IHardDelete ise:
→ EntityState.Deleted kalır → fiziksel silme
Global Query Filter Tanımı (proje context'inde)
BaseContext global query filter tanımlamaz — bu proje context'ine aittir:
public class AppDbContext(
DbContextOptions<AppDbContext> options,
ICurrentUserAccessor currentUserAccessor,
IDomainEventCollector? domainEventCollector = null)
: BaseContext(options, currentUserAccessor, domainEventCollector)
{
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
// Soft delete global query filter
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(BaseEntity).IsAssignableFrom(entityType.ClrType))
{
modelBuilder.Entity(entityType.ClrType)
.HasQueryFilter(e => !((BaseEntity)e).IsDeleted);
}
}
}
}
GetCurrentUser() (Bulk Operasyonlar İçin)
BaseContext üzerinde GetCurrentUser() metodu bulunur. Bu metot, ExecuteUpdateAsync ve ExecuteDeleteAsync gibi change tracker'ı atlayan bulk operasyonlarda audit bilgisi set etmek için Repository<T> tarafından kullanılır.
6. IRepository<T> ve Repository<T>
Write Metodları (Synchronous — SaveChanges çağırmaz)
void Add(T entity);
void AddRange(IEnumerable<T> entities);
void Remove(T entity);
void RemoveRange(IEnumerable<T> entities);
Bu metodlar yalnızca EF Core change tracker'a değişikliği bildirir. Veritabanına yazma işlemi, pipeline'daki TransactionBehaviour'ın CommitAsync() çağrısıyla gerçekleşir.
Handler'da
SaveChangesveyaCommitAsyncçağırma. Bu,TransactionBehaviour'ın sorumluluğudur.
Bulk Metodları (SQL Doğrudan — Change Tracker Bypass)
ExecuteUpdateAsync
Task<int> ExecuteUpdateAsync(
Expression<Func<T, bool>> predicate,
Action<UpdateSettersBuilder<T>> setters,
CancellationToken ct = default);
- EF Core
ExecuteUpdateAsyncile tek SQL UPDATE cümlesi yürütür - Change tracker atlanır — entity'ler
DbContext'e yüklenmez UpdatedDateveUpdatedByalanları otomatik olarak eklenir (audit bypass olmaz)- Domain event tetiklenmez (change tracker atlandığı için
CollectDomainEventsçalışmaz) CommitAsyncgerekmez — direkt veritabanına yazılır
// Tek alan güncelleme
int count = await uow.OrderRepository.ExecuteUpdateAsync(
predicate: x => x.CustomerId == customerId && x.Status == OrderStatus.Pending,
setters: s => s.SetProperty(e => e.Status, OrderStatus.Cancelled));
// Çoklu alan güncelleme
int count = await uow.OrderRepository.ExecuteUpdateAsync(
predicate: x => x.ExpiresAt < DateTime.UtcNow,
setters: s =>
{
s.SetProperty(e => e.Status, OrderStatus.Expired);
s.SetProperty(e => e.ExpiredAt, DateTime.UtcNow);
});
Uyarı:
ExecuteUpdateAsync, global query filter'larını (soft delete dahil) dikkate alır. Silinmiş kayıtları dahil etmek için EF CoreIgnoreQueryFilters()içeren özel bir sorgu gerekir.
ExecuteDeleteAsync
Task<int> ExecuteDeleteAsync(
Expression<Func<T, bool>> predicate,
CancellationToken ct = default);
IHardDeleteimplement eden entity → fiziksel silme (ExecuteDeleteAsync)IHardDeleteimplement etmeyen entity → soft delete (ExecuteUpdateAsyncileIsDeleted = true,DeletedDate,DeletedByset edilir)- Domain event tetiklenmez
CommitAsyncgerekmez
// Soft delete (IHardDelete yoksa)
await uow.AuditLogRepository.ExecuteDeleteAsync(x => x.CreatedDate < cutoffDate);
// Fiziksel silme (IHardDelete varsa)
await uow.TempDataRepository.ExecuteDeleteAsync(x => x.SessionId == sessionId);
Read Metodları (AsNoTracking)
Tüm read metodları AsNoTracking() ile çalışır — change tracking maliyeti yoktur.
// Tek kayıt
Task<T?> FirstOrDefaultAsync(
GenericExpression<T>? expression = null,
CancellationToken ct = default);
// Var mı kontrolü
Task<bool> AnyAsync(
GenericExpression<T>? expression = null,
CancellationToken ct = default);
// Tüm liste
Task<List<T>> ListAsync(
GenericExpression<T>? expression = null,
CancellationToken ct = default);
// Sayfalanmış liste
Task<PagedModel<T>> ListPagedAsync(
PagedFilterRequest<T> request,
CancellationToken ct = default);
// Sayım
Task<int> CountAsync(
GenericExpression<T>? expression = null,
CancellationToken ct = default);
7. GenericExpression<T>
Tek bir nesnede predicate, eager loading, sıralama ve query filter bypass'ı bir araya getirir.
public class GenericExpression<T> where T : BaseEntity
{
public Expression<Func<T, bool>>? Predicate { get; set; }
public Func<IQueryable<T>, IIncludableQueryable<T, object>>? IncludePaths { get; set; }
public Func<IQueryable<T>, IOrderedQueryable<T>>? OrderBy { get; set; }
public bool IgnoreQueryFilters { get; set; }
}
Constructor'lar
new GenericExpression<T>(predicate)
new GenericExpression<T>(predicate, includePaths)
new GenericExpression<T>(predicate, includePaths, orderBy)
// Static factory (daha okunabilir)
GenericExpression<T>.Create(predicate, includePaths, orderBy, ignoreQueryFilters)
Kullanım Örnekleri
// Yalnızca where
var expr = new GenericExpression<Order>(x => x.CustomerId == customerId);
// Where + include
var expr = new GenericExpression<Order>(
predicate: x => x.Id == orderId,
includePaths: q => q.Include(o => o.Lines)
.ThenInclude(l => l.Product));
// Sıralı liste
var expr = GenericExpression<Order>.Create(
predicate: x => x.Status == OrderStatus.Active,
orderBy: q => q.OrderByDescending(o => o.CreatedDate));
// Soft delete'i bypass et (silinmiş kayıtları da getir)
var expr = GenericExpression<Order>.Create(
predicate: x => x.Id == orderId,
ignoreQueryFilters: true);
// Tüm parametreler
var expr = GenericExpression<Order>.Create(
predicate: x => x.CustomerId == customerId && x.Status == OrderStatus.Active,
includePaths: q => q.Include(o => o.Lines).ThenInclude(l => l.Product),
orderBy: q => q.OrderBy(o => o.CreatedDate),
ignoreQueryFilters: false);
8. PagedFilterRequest<T>
ListPagedAsync için tüm parametreleri taşır.
public class PagedFilterRequest<T>
{
public PaginationFilterQuery FilterQuery { get; set; }
public Expression<Func<T, bool>>? Predicate { get; set; }
public Func<IQueryable<T>, IIncludableQueryable<T, object>>? IncludePaths { get; set; }
public Func<IQueryable<T>, IOrderedQueryable<T>>? OrderBy { get; set; }
public bool IgnoreQueryFilters { get; set; }
}
var request = new PagedFilterRequest<Order>(
filterQuery: pagedRequest.FilterQuery, // PageNumber, PageSize, Filters, Sorts
predicate: x => x.CustomerId == customerId,
includePaths: q => q.Include(o => o.Lines),
orderBy: q => q.OrderByDescending(o => o.CreatedDate));
var result = await uow.OrderRepository.ListPagedAsync(request, ct);
// result.TotalRecords — toplam kayıt
// result.PagedData — o sayfa için kayıtlar
Not:
FilterQuery.FilterItemsveFilterQuery.FilterGroupiçindeki dinamik filtreler,FilterQueryHelpertarafından repository katmanındapredicate'e eklenir.PagedFilterRequest.Predicateise handler tarafından önceden belirlenmiş sabit koşullar içindir — bu ikisi AND ile birleştirilir.
9. FilterQueryHelper ve SortQueryHelper
Bu yardımcılar doğrudan handler'lardan çağrılmaz; Repository<T>.ListPagedAsync içinde otomatik olarak devreye girer. Davranışlarını anlamak, client'tan kabul edilebilecek filtre yapısını tasarlamak açısından önemlidir.
FilterQueryHelper — Desteklenen Operatörler
| Operatör | Açıklama |
|---|---|
eq / is / equals |
Eşitlik |
neq / ne / not |
Eşitsizlik |
gt / greaterThan |
Büyüktür |
gte / greaterThanOrEqual |
Büyük eşit |
lt / lessThan |
Küçüktür |
lte / lessThanOrEqual |
Küçük eşit |
contains |
İçerir (yalnızca string) |
startsWith |
Başlar (yalnızca string) |
endsWith |
Biter (yalnızca string) |
isEmpty |
Null veya boş |
isNotEmpty |
Null değil ve boş değil |
FilterQueryHelper — Gelişmiş Özellikler
Dot-path navigasyon:
{ "columnField": "Customer.Name", "operatorValue": "contains", "value": "Ali" }
Koleksiyon navigasyon (Any/All pattern):
{ "columnField": "Lines.Product.Name", "operatorValue": "contains", "value": "Laptop" }
Koleksiyon sayısı:
{ "columnField": "Lines.Count", "operatorValue": "gt", "value": "3" }
Nested grup (parentheses mantığı):
{
"filterGroup": {
"logic": "or",
"items": [
{ "columnField": "Status", "operatorValue": "eq", "value": "Active" },
{ "columnField": "Status", "operatorValue": "eq", "value": "Pending" }
],
"groups": [
{
"negate": true,
"logic": "and",
"items": [
{ "columnField": "Amount", "operatorValue": "lt", "value": "100" }
]
}
]
}
}
Maksimum iç içe derinlik: 32
SortQueryHelper — Özellikler
{
"sorts": [
{ "field": "CreatedDate", "sort": "desc" },
{ "field": "Customer.Name", "sort": "asc" }
]
}
- Zincir sıralama: ilk alan
OrderBy, sonrakilerThenBy - Dot-path navigasyon desteği
- Koleksiyon alanı sıralaması: ascending →
Min(), descending →Max()aggregate kullanır
Güvenli Kullanım — Field Allow-List
Client'tan gelen ColumnField ve Sort.Field değerlerini doğrudan entity property'lerine bağlamak güvenlik riski taşır. Bir allow-list ile filtreleyin:
private static readonly HashSet<string> _allowedFilters = ["status", "createdDate", "amount"];
private static readonly HashSet<string> _allowedSorts = ["createdDate", "amount"];
// Handler başında kontrol et
if (request.FilterQuery.FilterItems?.Any(f => !_allowedFilters.Contains(f.ColumnField)) == true)
return Result.Fail(ResultStatus.BadRequest, "Geçersiz filtre alanı.");
10. Dapper Entegrasyonu
Neden Dapper?
EF Core, karmaşık JOIN'ler veya yüksek performanslı raporlama sorgularında ağır gelebilir. DapperExecutor, EF Core'un aktif transaction'ını paylaşarak aynı iş biriminde ham SQL çalıştırmayı sağlar.
IDapperExecutor
public interface IDapperExecutor
{
Task<IReadOnlyList<T>> QueryAsync<T>(
string sql,
object? param = null,
CommandType? commandType = null,
CancellationToken cancellationToken = default);
Task<T?> QuerySingleOrDefaultAsync<T>(
string sql,
object? param = null,
CommandType? commandType = null,
CancellationToken cancellationToken = default);
Task<int> ExecuteAsync(
string sql,
object? param = null,
CommandType? commandType = null,
CancellationToken cancellationToken = default);
}
EF Core Transaction ile Birlikte Kullanım
DapperExecutor, IDbConnectionProvider üzerinden EF Core'un DbConnection ve aktif DbTransaction'ını alır. ICommand handler'larında TransactionBehaviour transaction'ı başlatmışsa Dapper da aynı transaction içinde çalışır:
internal sealed class ProcessOrderCommandHandler(
IUnitOfWork uow,
IDapperExecutor dapper) : IRequestHandler<ProcessOrderCommand, Result>
{
public async Task<Result> Handle(ProcessOrderCommand request, CancellationToken ct)
{
// EF Core ile entity oku
var order = await uow.OrderRepository.FirstOrDefaultAsync(
new GenericExpression<Order>(x => x.Id == request.OrderId), ct);
if (order is null)
return Result.Fail(ResultStatus.NotFound, "Sipariş bulunamadı.");
// Dapper ile ham SQL çalıştır — aynı transaction
await dapper.ExecuteAsync(
"UPDATE InventoryItems SET Reserved = Reserved + @Qty WHERE ProductId = @ProductId",
new { Qty = request.Quantity, ProductId = order.ProductId },
cancellationToken: ct);
order.Process();
uow.OrderRepository.Add(order); // aslında Update — tracking'e zaten ekliydi
return Result.Success(ResultStatus.Success);
// TransactionBehaviour commit eder — hem EF hem Dapper aynı transaction'da tamamlanır
}
}
Dapper ile Raporlama Sorgusu (IQuery)
IQuery handler'larında transaction açılmaz; Dapper bağımsız olarak çalışır:
public record GetOrderSummaryQuery(DateOnly From, DateOnly To)
: IRequest<Result<List<OrderSummaryDto>>>, IQuery;
internal sealed class GetOrderSummaryQueryHandler(IDapperExecutor dapper)
: IRequestHandler<GetOrderSummaryQuery, Result<List<OrderSummaryDto>>>
{
private const string Sql = """
SELECT
o.Status,
COUNT(*) AS Count,
SUM(o.Amount) AS TotalAmount
FROM Orders o
WHERE o.CreatedDate BETWEEN @From AND @To
AND o.IsDeleted = 0
GROUP BY o.Status
""";
public async Task<Result<List<OrderSummaryDto>>> Handle(
GetOrderSummaryQuery request, CancellationToken ct)
{
var rows = await dapper.QueryAsync<OrderSummaryDto>(
Sql,
new { request.From, request.To },
cancellationToken: ct);
return Result<List<OrderSummaryDto>>.Success(
ResultStatus.Success, [.. rows]);
}
}
Not:
IQueryhandler'larında transaction açılmadığı için Dapper bağımsız connection ile çalışır. Transaction gerektiren sorgu+yazma kombinasyonu içinIQueryWithSideEffectkullanın.
11. Entegrasyon ve DI Kaydı
AddRepositories<TContext>()
public static IServiceCollection AddRepositories<TContext>(this IServiceCollection services)
where TContext : DbContext
Bu extension method yalnızca şunları kaydeder:
services.AddScoped<IDbConnectionProvider, EfCoreDbConnectionProvider<TContext>>();
services.AddScoped<IDapperExecutor, DapperExecutor>();
Proje-özgü repository'ler ve IUnitOfWork bu metodun dışındadır — projenin Infrastructure DI kaydına aittir.
Program.cs'te Minimum Kurulum
// DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
// Dapper + IDbConnectionProvider
builder.Services.AddRepositories<AppDbContext>();
// ICurrentUserAccessor (Ledbim.AspNetCore gerektirir)
builder.Services.AddScoped<ICurrentUserAccessor, HttpContextCurrentUserAccessor>();
// Domain event collector (Ledbim.Core)
builder.Services.AddScoped<IDomainEventCollector, DomainEventCollector>();
// IUnitOfWork (proje-özgü)
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<ITransactionManager>(
sp => sp.GetRequiredService<IUnitOfWork>());
12. Proje Bazlı Kurulum (IUnitOfWork)
Ledbim.EntityFramework IUnitOfWork'ü sağlamaz; projeye özeldir. Aşağıdaki şablon takip edilmelidir.
Repository Interface'i (Application katmanı)
public interface IOrderRepository : IRepository<Order>
{
// Proje-özgü ek metodlar buraya
Task<Order?> GetWithLinesAsync(Guid orderId, CancellationToken ct = default);
}
Repository Implementasyonu (Infrastructure katmanı)
public sealed class OrderRepository(AppDbContext context)
: Repository<Order>(context), IOrderRepository
{
public async Task<Order?> GetWithLinesAsync(Guid orderId, CancellationToken ct = default)
=> await FirstOrDefaultAsync(
new GenericExpression<Order>(
predicate: x => x.Id == orderId,
includePaths: q => q.Include(o => o.Lines)
.ThenInclude(l => l.Product)),
ct);
}
IUnitOfWork (Application katmanı)
public interface IUnitOfWork : ITransactionManager
{
IOrderRepository OrderRepository { get; }
ICustomerRepository CustomerRepository { get; }
}
UnitOfWork (Infrastructure katmanı)
public sealed class UnitOfWork(
AppDbContext context,
IOrderRepository orderRepository,
ICustomerRepository customerRepository) : IUnitOfWork
{
public IOrderRepository OrderRepository => orderRepository;
public ICustomerRepository CustomerRepository => customerRepository;
public bool HasActiveTransaction => context.Database.CurrentTransaction is not null;
public async Task BeginTransactionAsync(CancellationToken ct = default)
=> await context.Database.BeginTransactionAsync(ct);
public async Task CommitAsync(CancellationToken ct = default)
{
await context.SaveChangesAsync(ct);
await context.Database.CurrentTransaction!.CommitAsync(ct);
}
public async Task RollbackAsync(CancellationToken ct = default)
=> await context.Database.CurrentTransaction!.RollbackAsync(ct);
}
DI Kaydı (Infrastructure katmanı)
// Repository'ler
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<ICustomerRepository, CustomerRepository>();
// UnitOfWork
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<ITransactionManager>(
sp => sp.GetRequiredService<IUnitOfWork>());
13. Kullanım Örnekleri
Command Handler — Oluşturma
internal sealed class CreateOrderCommandHandler(IUnitOfWork uow)
: IRequestHandler<CreateOrderCommand, Result<Guid>>
{
public async Task<Result<Guid>> Handle(CreateOrderCommand request, CancellationToken ct)
{
var exists = await uow.CustomerRepository.AnyAsync(
new GenericExpression<Customer>(x => x.Id == request.CustomerId), ct);
if (!exists)
return Result<Guid>.Fail(ResultStatus.NotFound, "Müşteri bulunamadı.");
var order = Order.Create(request.CustomerId, request.Lines);
uow.OrderRepository.Add(order);
// TransactionBehaviour CommitAsync çağırır — SaveChanges burada yapılmaz
return Result<Guid>.Success(ResultStatus.Created, order.Id, "Sipariş oluşturuldu.");
}
}
Command Handler — Güncelleme
internal sealed class UpdateOrderStatusCommandHandler(IUnitOfWork uow)
: IRequestHandler<UpdateOrderStatusCommand, Result>
{
public async Task<Result> Handle(UpdateOrderStatusCommand request, CancellationToken ct)
{
var order = await uow.OrderRepository.FirstOrDefaultAsync(
new GenericExpression<Order>(x => x.Id == request.OrderId), ct);
if (order is null)
return Result.Fail(ResultStatus.NotFound, "Sipariş bulunamadı.");
var updateResult = order.UpdateStatus(request.NewStatus);
if (!updateResult.IsSuccess)
return updateResult;
// Entity change tracker'da — Remove gerekmez, Modified olarak işaretlenir
return Result.Success(ResultStatus.Success, "Durum güncellendi.");
}
}
Command Handler — Silme
internal sealed class DeleteOrderCommandHandler(IUnitOfWork uow)
: IRequestHandler<DeleteOrderCommand, Result>
{
public async Task<Result> Handle(DeleteOrderCommand request, CancellationToken ct)
{
var order = await uow.OrderRepository.FirstOrDefaultAsync(
new GenericExpression<Order>(x => x.Id == request.OrderId), ct);
if (order is null)
return Result.Fail(ResultStatus.NotFound, "Sipariş bulunamadı.");
uow.OrderRepository.Remove(order);
// IHardDelete değilse → soft delete (BaseContext halleder)
return Result.Success(ResultStatus.NoContent);
}
}
Query Handler — Sayfalı Liste
internal sealed class ListOrdersQueryHandler(IUnitOfWork uow)
: IRequestHandler<ListOrdersQuery, PagedResponse<IEnumerable<OrderSummaryDto>>>
{
public async Task<PagedResponse<IEnumerable<OrderSummaryDto>>> Handle(
ListOrdersQuery request, CancellationToken ct)
{
var filterRequest = new PagedFilterRequest<Order>(
filterQuery: request.FilterQuery,
predicate: request.CustomerId.HasValue
? x => x.CustomerId == request.CustomerId.Value
: null,
includePaths: q => q.Include(o => o.Customer));
var result = await uow.OrderRepository.ListPagedAsync(filterRequest, ct);
return PagedHelper.CreatePagedResponse(
data: result.PagedData.Select(o => o.ToSummaryDto()),
totalRecords: result.TotalRecords,
pageNumber: request.FilterQuery.PageNumber,
pageSize: request.FilterQuery.PageSize);
}
}
Bulk Update
// Tüm bekleyen siparişleri toplu iptal et
int cancelled = await uow.OrderRepository.ExecuteUpdateAsync(
predicate: x => x.Status == OrderStatus.Pending
&& x.CreatedDate < DateTime.UtcNow.AddDays(-7),
setters: s =>
{
s.SetProperty(e => e.Status, OrderStatus.Cancelled);
s.SetProperty(e => e.CancelledAt, DateTime.UtcNow);
});
14. Mimari Notlar
Bu Paket Hangi Katmana Aittir?
Ledbim.EntityFramework Infrastructure katmanının temel bağımlılığıdır:
Domain → Ledbim.Core (BaseEntity, AggregateRoot)
Application → Ledbim.Core (IUnitOfWork kontratı, ICommand/IQuery, Result)
Infrastructure → Ledbim.EntityFramework (BaseContext, Repository<T>, Dapper)
API → Ledbim.AspNetCore
Hangi Paketlerle Birlikte Kullanılır?
| Paket | Birlikte Kullanım Nedeni |
|---|---|
Ledbim.Core |
Zorunlu bağımlılık |
Ledbim.AspNetCore |
ICurrentUserAccessor implementasyonu + pipeline behavior DI kaydı |
Ledbim.Security |
ICurrentUserAccessor JWT claim'inden okuyorsa |
15. Dikkat Edilmesi Gerekenler
| Durum | Açıklama |
|---|---|
Handler'da SaveChanges / CommitAsync çağırma |
Kesinlikle yapılmaz. TransactionBehaviour halleder. Yapılırsa çift commit riski ve domain event'lerin iki kez dispatch edilmesi olur. |
Remove() fiziksel silmez |
IHardDelete implement etmeyen entity'ler soft delete'e dönüşür. Fiziksel silme için IHardDelete ekleyin. |
ExecuteUpdateAsync domain event tetiklemez |
Bulk işlemlerde aggregate metodu çağrılmadığı için domain event üretilemez. Gerekiyorsa event'i handler'dan manuel IDomainEventCollector.AddNotification() ile ekleyin. |
ExecuteUpdateAsync change tracker'ı atlar |
In-memory'de yüklü entity'ler stale kalır. Bulk işlem sonrası aynı entity'yi read ediyorsanız AsNoTracking ile yeniden yükleyin. |
IgnoreQueryFilters = true dikkatli |
Global soft delete filtresini bypass eder. Yalnızca admin/restore senaryolarında kullanın. |
GenericExpression<T> constraint |
where T : BaseEntity — yalnızca BaseEntity türevleri için çalışır. |
IDomainEventCollector nullable constructor |
Geçilmezse domain event'ler toplanmaz. Aggregate kullanan projelerde Scoped olarak register edilmeli. |
Dapper IQuery handler'larında transaction yok |
IQuery'de TransactionBehaviour transaction açmaz; Dapper bağımsız connection kullanır. |
| Column adı eşleşmesi (Dapper) | Dapper, SQL sütun adlarını C# property adlarıyla eşleştirir. snake_case sütunlar için DapperExtensions veya DTO alias kullanın. |
AddRepositories<TContext>() yalnızca Dapper'ı kaydeder |
Proje repository'leri ve IUnitOfWork ayrıca kayıt edilmelidir. |
16. Hızlı Başlangıç
// ── 1. DbContext ──────────────────────────────────────────────────
public class AppDbContext(
DbContextOptions<AppDbContext> options,
ICurrentUserAccessor currentUserAccessor,
IDomainEventCollector? domainEventCollector = null)
: BaseContext(options, currentUserAccessor, domainEventCollector)
{
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder mb)
{
mb.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
foreach (var et in mb.Model.GetEntityTypes())
if (typeof(BaseEntity).IsAssignableFrom(et.ClrType))
mb.Entity(et.ClrType).HasQueryFilter(
e => !((BaseEntity)e).IsDeleted);
}
}
// ── 2. Repository ────────────────────────────────────────────────
public interface IOrderRepository : IRepository<Order> { }
public sealed class OrderRepository(AppDbContext ctx)
: Repository<Order>(ctx), IOrderRepository { }
// ── 3. UnitOfWork ────────────────────────────────────────────────
public interface IUnitOfWork : ITransactionManager
{
IOrderRepository Orders { get; }
}
public sealed class UnitOfWork(AppDbContext ctx, IOrderRepository orders) : IUnitOfWork
{
public IOrderRepository Orders => orders;
public bool HasActiveTransaction => ctx.Database.CurrentTransaction is not null;
public Task BeginTransactionAsync(CancellationToken ct = default)
=> ctx.Database.BeginTransactionAsync(ct);
public async Task CommitAsync(CancellationToken ct = default)
{
await ctx.SaveChangesAsync(ct);
await ctx.Database.CurrentTransaction!.CommitAsync(ct);
}
public Task RollbackAsync(CancellationToken ct = default)
=> ctx.Database.CurrentTransaction!.RollbackAsync(ct);
}
// ── 4. Program.cs ────────────────────────────────────────────────
builder.Services.AddDbContext<AppDbContext>(o =>
o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddRepositories<AppDbContext>();
builder.Services.AddScoped<IDomainEventCollector, DomainEventCollector>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<ITransactionManager>(
sp => sp.GetRequiredService<IUnitOfWork>());
// ── 5. Handler ───────────────────────────────────────────────────
internal sealed class CreateOrderCommandHandler(IUnitOfWork uow)
: IRequestHandler<CreateOrderCommand, Result<Guid>>
{
public async Task<Result<Guid>> Handle(CreateOrderCommand req, CancellationToken ct)
{
var order = Order.Create(req.CustomerId, req.Lines);
uow.Orders.Add(order);
return Result<Guid>.Success(ResultStatus.Created, order.Id);
}
}
| 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
- Dapper (>= 2.1.66)
- Ledbim.Core (>= 1.2.0)
- Microsoft.EntityFrameworkCore.SqlServer (>= 10.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.2.0 | 166 | 3/28/2026 |