Skip to main content
DiyanetCleanArchitectureDbContext data katmanının merkezidir. Hem klasik DbContext’tir hem de IUnitOfWork arayüzünü uygular — yani transaction sınırı ve domain event dispatch noktası buradadır.
public class DiyanetCleanArchitectureDbContext : DbContext, IUnitOfWork
{
    public const string DEFAULT_SCHEMA   = "public";
    public const string MESSAGING_SCHEMA = "messaging";
    // ...
}

DbSet’ler

Tüm aggregate root’lar (ve bazı child entity’ler) DbSet olarak tanımlıdır:
#region DbSet — User & Role
public DbSet<User>           Users           { get; set; }
public DbSet<Role>           Roles           { get; set; }
public DbSet<Permission>     Permissions     { get; set; }
public DbSet<RolePermission> RolePermissions { get; set; }
public DbSet<UserRole>       UserRoles       { get; set; }
#endregion

public DbSet<Citizen>      Citizens      { get; set; }   // website kullanıcıları — Personel'den ayrı aggregate
public DbSet<Organization> Organizations { get; set; }
public DbSet<Branch>       Branches      { get; set; }
public DbSet<StaffMember>  StaffMembers  { get; set; }
public DbSet<City>         Cities        { get; set; }   // Enumeration
public DbSet<District>     Districts     { get; set; }

public DbSet<Faq>          Faqs          { get; set; }
public DbSet<Announcement> Announcements { get; set; }
public DbSet<Center>       Centers       { get; set; }
public DbSet<SiteSettings> SiteSettings  { get; set; }

public DbSet<BagisBasvuruPlan>    BagisBasvuruPlanlari    { get; set; }
public DbSet<BagisBasvuru>        BagisBasvurulari        { get; set; }
public DbSet<EtkinlikBasvuruPlan> EtkinlikBasvuruPlanlari { get; set; }
public DbSet<EtkinlikBasvuru>     EtkinlikBasvurulari     { get; set; }

public DbSet<LegalDocument>        LegalDocuments        { get; set; }
public DbSet<LegalDocumentVersion> LegalDocumentVersions { get; set; }
public DbSet<AdminNotification>     AdminNotifications     { get; set; }
public DbSet<AdminNotificationRead> AdminNotificationReads { get; set; }
public DbSet<SupportTicket>              SupportTickets             { get; set; }
public DbSet<SupportTicketStatusHistory> SupportTicketStatusHistory { get; set; }
public DbSet<SupportTicketComment>       SupportTicketComments      { get; set; }
public DbSet<SupportTicketAttachment>    SupportTicketAttachments   { get; set; }
Citizen (vatandaş portalı) ile User (personel/admin) ayrı aggregate’lerdir ve ayrı tablolara yazılır. Aynı kişi iki tarafta da bağımsız kayıt tutar.

OnModelCreating

Üç iş yapar: configuration’ları toplar, MassTransit messaging tablolarını ekler, global soft-delete filtresini kurar.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 1) Tüm IEntityTypeConfiguration<T>'lar assembly'den toplanır
    modelBuilder.ApplyConfigurationsFromAssembly(typeof(DiyanetCleanArchitectureDbContext).Assembly);

    // 2) MassTransit EF Core Outbox + Inbox — `messaging` şemasında
    modelBuilder.AddInboxStateEntity(e   => e.ToTable("inbox_state",    MESSAGING_SCHEMA));
    modelBuilder.AddOutboxStateEntity(e  => e.ToTable("outbox_state",   MESSAGING_SCHEMA));
    modelBuilder.AddOutboxMessageEntity(e => e.ToTable("outbox_message", MESSAGING_SCHEMA));

    // 3) Global soft-delete query filter — ISoftDeletable uygulayan her tip için
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        if (!typeof(ISoftDeletable).IsAssignableFrom(entityType.ClrType))
            continue;

        var param  = Expression.Parameter(entityType.ClrType, "e");
        var prop   = Expression.Property(param, nameof(ISoftDeletable.DeletedAt));
        var body   = Expression.Equal(prop, Expression.Constant(null, typeof(DateTime?)));
        var lambda = Expression.Lambda(body, param);

        modelBuilder.Entity(entityType.ClrType).HasQueryFilter(lambda);
    }

    base.OnModelCreating(modelBuilder);
}
Global filtre her ISoftDeletable için DeletedAt == null koşulu ekler — detaylar Soft Delete sayfasında.

UnitOfWork: SaveEntitiesAsync

SaveEntitiesAsync business akışının kullandığı kayıt metodudur. SaveChanges çağırır, ardından değişen aggregate’lerin domain event’lerini dispatch eder:
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
{
    // AuditInterceptor SavingChanges anında devreye girer — burada ekstra çağrı yok
    int result = await base.SaveChangesAsync(cancellationToken);

    if (result > 0)
        await _mediator.DispatchDomainEventsAsync(this);

    return true;
}
DispatchDomainEventsAsync (SeedWork/MediatorExtension.cs) change tracker’daki her Entity’nin event’lerini toplar, temizler ve tek tek yayar. IntegrationEvent’ler IEventBus (MassTransit Outbox), kalan domain event’ler MediatR ile in-memory işlenir:
foreach (var domainEvent in domainEvents)
{
    try
    {
        if (domainEvent is IBusEvent busEvent)
        {
            if (busEvent is IntegrationEvent integrationEvent)
                await ctx._eventBus.PublishAsync(integrationEvent);
        }
        else
        {
            await mediator.Publish(domainEvent);
        }
    }
    catch (Exception ex)
    {
        // Akış kesilmesin — kalan event'ler işlensin; ama silent failure olmasın
        ctx.GetService<ILoggerFactory>()?
           .CreateLogger(typeof(MediatorExtension))
           .LogError(ex, "[DomainEventDispatch] Event işlenemedi: {EventType}", domainEvent.GetType().Name);
    }
}
Bir event handler’ı patlarsa akış kesilmez (catch/continue) ama hata DbContext logger’ına yazılır. Eski versiyonda catch sessizdi ve hatalar görünmüyordu.
DbContext ayrıca manuel transaction desteği sunar: BeginTransactionAsync (ReadCommitted), CommitTransactionAsync, RollbackTransaction. Çoğu komut tek SaveEntitiesAsync ile yetinir; çapraz-aggregate atomikliği gereken yerlerde açık transaction kullanılır.

EntityTypeConfiguration örnekleri

Value object’leri kolona çevirme — HasConversion

User.Email, User.Phone, User.FullName value object’tir; tek kolona serialize edilir:
builder.Property(u => u.Email)
    .HasConversion(e => e.Value, v => new Email(v))
    .HasColumnName("email")
    .HasMaxLength(255)
    .IsRequired(false);

builder.Property(u => u.Phone)
    .HasConversion(p => p.Value, v => new Phone(v))
    .HasColumnName("phone")
    .HasMaxLength(25)
    .IsRequired(false);

Owned type — OwnsOne

TOTP konfigürasyonu nested owned type’tır (aynı users satırına kolon olarak yazılır):
builder.OwnsOne(u => u.Totp, totp =>
{
    totp.Property(t => t.IsEnabled).HasColumnName("totp_enabled");
    totp.Property(t => t.LastUsedTimeStep).HasColumnName("totp_last_used_time_step");

    totp.OwnsOne(t => t.Secret, secret =>
    {
        secret.Property(s => s.EncryptedValue).HasColumnName("totp_secret").IsRequired();
    });
});
Citizen.ProfileImage ise owned type olmasına rağmen ayrı tabloya (citizen_profile_images) yazılır — bytea payload’ı liste sorgularını şişirmesin diye AsSplitQuery ile lazy yüklenir:
builder.OwnsOne(v => v.ProfileImage, img =>
{
    img.ToTable("citizen_profile_images", DiyanetCleanArchitectureDbContext.DEFAULT_SCHEMA);
    img.Property(p => p.Veri).HasColumnName("veri").HasColumnType("bytea");
    img.Property(p => p.ContentType).HasColumnName("content_type").HasMaxLength(50);
});

DB-generated referans numarası — IDENTITY kolon

User.ReferenceNumber PostgreSQL IDENTITY kolonudur (insan-okuyabilir, artan numara), 100’den başlar ve insert sonrası salt-okunur:
builder.Property(u => u.ReferenceNumber)
    .HasColumnName("reference_number")
    .HasColumnType("bigint")
    .ValueGeneratedOnAdd()
    .UseIdentityAlwaysColumn()          // PostgreSQL GENERATED ALWAYS AS IDENTITY
    .HasIdentityOptions(startValue: 100);

builder.Metadata
    .FindProperty(nameof(User.ReferenceNumber))!
    .SetAfterSaveBehavior(PropertySaveBehavior.Ignore); // read-only after insert

Optimistic concurrency

users tablosu shadow row_version kolonuyla optimistic concurrency control yapar:
builder.Property<byte[]>("row_version").IsRowVersion();

Repository’ler

İki ayrı path var: write (IRepository<T>) ve read (IReadRepository<T>). İkisi de Ardalis.Specification tabanlıdır.

EFRepository — write tarafı

public class EFRepository<T> : RepositoryBase<T>, IRepository<T> where T : class, IAggregateRoot
{
    protected readonly DiyanetCleanArchitectureDbContext _context;
    public IUnitOfWork UnitOfWork => _context;

    public EFRepository(DiyanetCleanArchitectureDbContext context) : base(context) { /* ... */ }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        throw new Exception("Dont use this SaveChangesAsync() It's forbidden!");
    }
}
EFRepository.SaveChangesAsync bilerek exception fırlatır. Kayıt repository üzerinden değil, UnitOfWork.SaveEntitiesAsync (yani DbContext) üzerinden yapılır — böylece domain event dispatch atlanmaz. Repository’ler sadece change tracker’ı düzenler (AddAsync, UpdateAsync, DeleteAsync).
UnitOfWork property’si _context’in ta kendisidir; handler _repository.UnitOfWork.SaveEntitiesAsync(...) ile kaydeder.

CachedRepository — read tarafı (HybridCache decorator)

IReadRepository<T> implementasyonu, gerçek sorguyu EFCacheRepository<T>’ye delege eder ve spec cache-enabled ise HybridCache (L1 memory + L2 Redis) ile sarmalar:
public async Task<List<T>> ListAsync(ISpecification<T> spec, CancellationToken ct = default)
{
    if (CacheDisabled(spec))
        return await _source.ListAsync(spec, ct);

    var key = $"{spec.CacheKey}:List";
    return await _cache.GetOrCreateAsync(
        key,
        async c => await _source.ListAsync(spec, c),
        BuildOptions(spec),
        TagsOf(spec),
        ct);
}
Cache yalnızca iki şart birden sağlanınca devreye girer: global Cache:Enabled açık ve spec’te EnableCache(...) zinciri kurulmuş olmalı:
private bool CacheDisabled<TSpec>(TSpec spec) where TSpec : ISpecification<T>
    => !_options.CurrentValue.Enabled
       || !spec.CacheEnabled
       || string.IsNullOrWhiteSpace(spec.CacheKey);
TTL ve tag’ler spec’ten okunur; TTL belirtilmemişse config’teki Cache:L2:Ttl kullanılır. Handler/query cache’ten habersizdir — cache tamamen decorator’ın işidir.

DI kaydı

AddInfrastructureEFCore (DependencyInjection.cs) tüm bağımlılıkları kurar:
services.AddDbContext<DiyanetCleanArchitectureDbContext>((sp, options) =>
{
    options.UseNpgsql(configuration.GetConnectionString("DiyanetCleanArchitectureDatabaseConnection"))
           .UseSnakeCaseNamingConvention();
    // AuditInterceptor scoped olduğu için buraya EKLENMIYOR — OnConfiguring'de ekleniyor.
});

services.AddScoped(typeof(IReadRepository<>), typeof(CachedRepository<>));
services.AddScoped(typeof(IRepository<>),     typeof(EFRepository<>));
services.AddScoped(typeof(EFRepository<>));
services.AddScoped(typeof(EFCacheRepository<>));

// UnitOfWork = DbContext'in kendisi
services.AddScoped<IUnitOfWork>(sp => sp.GetRequiredService<DiyanetCleanArchitectureDbContext>());
Connection string adı sabittir: DiyanetCleanArchitectureDatabaseConnection. Aynı isim DesignTimeDbContextFactoryBase tarafından da kullanılır (migration araçları için).

İlgili

Audit Interceptor

AuditInterceptor’ın SaveChanges anında nasıl çalıştığı.

Soft Delete

Global query filter ve restore senaryoları.

Migrations

Bu DbContext’ten migration üretme ve uygulama.

Data Genel Bakış

Katmanın rolü, şemalar, klasör yapısı.