Ledbim.EntityFramework 1.2.0

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

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

  1. Paket Amacı
  2. Paket Kapsamı
  3. Bağımlılıklar
  4. Önemli Yapılar
  5. BaseContext
  6. IRepository<T> ve Repository<T>
  7. GenericExpression<T>
  8. PagedFilterRequest<T>
  9. FilterQueryHelper ve SortQueryHelper
  10. Dapper Entegrasyonu
  11. Entegrasyon ve DI Kaydı
  12. Proje Bazlı Kurulum (IUnitOfWork)
  13. Kullanım Örnekleri
  14. Mimari Notlar
  15. Dikkat Edilmesi Gerekenler
  16. 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, DeletedBy alanları SaveChangesAsync sırasında otomatik doldurulur.
  • Soft delete: Remove() çağrıları IHardDelete uygulamayan entity'leri fiziksel silmek yerine IsDeleted = true olarak işaretler.
  • Change-tracking repository: Handler'lar SaveChanges çağırmaz; bu sorumluluk pipeline'daki TransactionBehaviour'a aittir.
  • Bulk operasyonlar: ExecuteUpdateAsync ve ExecuteDeleteAsync, 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 SaveChanges veya CommitAsync ç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 ExecuteUpdateAsync ile tek SQL UPDATE cümlesi yürütür
  • Change tracker atlanır — entity'ler DbContext'e yüklenmez
  • UpdatedDate ve UpdatedBy alanları otomatik olarak eklenir (audit bypass olmaz)
  • Domain event tetiklenmez (change tracker atlandığı için CollectDomainEvents çalışmaz)
  • CommitAsync gerekmez — 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 Core IgnoreQueryFilters() içeren özel bir sorgu gerekir.

ExecuteDeleteAsync
Task<int> ExecuteDeleteAsync(
    Expression<Func<T, bool>> predicate,
    CancellationToken ct = default);
  • IHardDelete implement eden entity → fiziksel silme (ExecuteDeleteAsync)
  • IHardDelete implement etmeyen entity → soft delete (ExecuteUpdateAsync ile IsDeleted = true, DeletedDate, DeletedBy set edilir)
  • Domain event tetiklenmez
  • CommitAsync gerekmez
// 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.FilterItems ve FilterQuery.FilterGroup içindeki dinamik filtreler, FilterQueryHelper tarafından repository katmanında predicate'e eklenir. PagedFilterRequest.Predicate ise 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, sonrakiler ThenBy
  • 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: IQuery handler'larında transaction açılmadığı için Dapper bağımsız connection ile çalışır. Transaction gerektiren sorgu+yazma kombinasyonu için IQueryWithSideEffect kullanı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 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

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