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ı.