Bir DBContext’in Çeşitli Senaryolar İçin Özelleştirilmesi
Selamlar,
Bu makalede, ihtiyaca göre çeşitli senaryolarda, .NET 5.0 üzerinde bir DBContext’i nasıl özelleştirebileceğimizi inceleyeceğiz.
İlk olarak eğer uygulamamız DBFirst ise, yani var olan bir DB üzerinde Entitylerimizi aşağıdaki gibi bir komut ile oluşturuldu ise, oluşan DBContext’den inheritance alınarak yeni bir CustomContext oluşturulur. Eğer bu işlem yapılmaz ise, var olan DBContext üzerinde yapılan değişiklikler, aşağıdaki komutun tekrarında ezilecektir.
1 2 3 4 |
Scaffold-DBContext "Data Source=192.168.11.160;Initial Catalog=BLOG_TEST;Persist Security Info=True; User ID=blogguser;Password=abcd666999; pooling=True;min pool size=0;max pool size=100;MultipleActiveResultSets=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Entities -ContextDir "Entities\DbContexts" -Context DashboardContext -Force -Project Dashboard.DB -StartupProject Dashboard.DB |
DB/BlogContext : DBContext’den oluşturulan, tüm özelleştirmelerin yapılacağı custom context.
1 2 3 4 5 6 |
public class BlogContext : DashboardContext { public BlogContext() { } } |
Resim kaynağı: https://woxapp.com/uploads/images/4_MVVM.png
1.Durum DB’de olmayan Custom bir Model’in DBContext’de varmış gibi gösterilmesi:
Amaç Repository katmanı kullanılarak, istenen Sql komutunun çalıştırılması ve geriye, Database’de olmayan bir ViewModel(CustomMenuModel)’in döndürülmesidir.
Model/CustomMenuModel.cs: Aşağıda görüldüğü gibi DB’de olmayan ViewModel, Menu oluşturmak amacı ile kullanılmıştır. BaseEntity’den türetilmiştir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System; using System.Collections.Generic; using System.Text; namespace Dashboard.Core.Models.Menu { public class CustomMenuModel : BaseEntity { public int Key { get; set; } public int IdMenu { get; set; } public int OrderNumber { get; set; } public string MenuName { get; set; } public int? IdSecurityController { get; set; } public long? ActionNumber { get; set; } public string RoutingPath { get; set; } public string ImageClass { get; set; } public int IdMenuParent { get; set; } public bool IsParent { get; set; } public bool HasChild { get; set; } public long? ActionNumberTotal { get; set; } public int? IdUser { get; set; } } } |
Repository katmanı aşağıda görüldüğü gibi BaseEntity’den türeyen bir sınıf beklemektedir. Bundan dolayı ilgili CustoMenuModel, BaseEntity‘den türetilmiştir.
1 |
public class GeneralRepository<T> : IRepository<T> where T : BaseEntity |
Şimdi sıra geldi bu CustomModeli DBContex’e tanıtmaya.
Dashboard.DB/PartialEntites/BlogContext: Aşağıda görüldüğü gibi “CustomMenuModel“, Entity tarafında tanımlanırken hata alınmaması amacı ile, “OnModelCreating()” methodunda “entity.HasNoKey()” olarak işaretlenmiştir. BlogContext’e tanımlanan CustomMenuModel’e, repository katmanında artık rahatlıkla erişilebilecektir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class BlogContext : DashboardContext { public BlogContext() { } public DbSet<CustomMenuModel> CustomMenuModel { get; set; } public BlogContext(DbContextOptions<DashboardContext> options) : base(options){} protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<CustomMenuModel>(entity => { entity.HasNoKey(); }); } } |
Service/MenuService.cs: Aşağıdaki örnekde, SlqDB tarafında tanımlı bir “fn.GetMenu()“‘den dönen function sonuçları, CustomMenuModel olarak geri dönülmüş ve Repository katmanında ilgili ViewModel,sanki DB’de varmış gibi, DBContext üzerinden sorgulama imkanı sağlamıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
using AutoMapper; using Dashboard.Core.Models.Menu; using Dashboard.DB.PartialEntites; using Dashboard.Repository; using System; using System.Linq; namespace Dashboard.Services.Menu { public class MenuService : IMenuService { private readonly IRepository<CustomMenuModel> _customMenuRepository; // DB'de yok ViewModel'e ait Repository private readonly IRepository<DB.Entities.DbUser> _usersRepository; //DB'de var. Gerçek Entity'e ait Repository. public MenuService( IRepository<CustomMenuModel> customMenuRepository, IRepository<DB.Entities.DbUser> usersRepository; IMapper mapper ) { _mapper = mapper; _customMenuRepository = customMenuRepository; _usersRepository=usersRepository; } public CustomMenuModel GetMenuListByUserId(int userId) { int? isAdmin = 0; isAdmin = _usersRepository.GetById(userId).IsAdmin==true?1:0; isAdmin = isAdmin == null ? 0 : isAdmin; var result = _customMenuRepository.GetSql($"SELECT * FROM [dbo].[fn_GetMenu]({userId},{isAdmin}) ORDER BY OrderNumber"); return result; } } } |
Repository/GeneralRepository.cs => GetSql(): Aşağıda görüldüğü gibi, CustomMenuModel DB’de olmadığı halde “GetSql()” methodunda, BlogContext üzerinden tanımlı şekilde erişilebilmektedir. İlgili method’da kendisine parametre olarak verilen Raw SqlQuery, Execute edilmektedir. Ayrıca Repository’de kullanılacak tüm Entitylerin, BaseEntity sınıfından türetilmesi zorunlu hale getirilmiştir. Makalenin devamında, ilgili Entitylerin “BaseEntity“‘den nasıl türetildiği gösterilecektir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
namespace Dashboard.Repository { public class GeneralRepository<T> : IRepository<T> where T : BaseEntity { private readonly BlogContext _context; public GeneralRepository(BlogContext context) { _context = context; } protected virtual DbSet<T> Entities => _entities ?? (_entities = _context.Set<T>()); public IEnumerable<T> GetSql(string sql) { return Entities.FromSqlRaw(sql).AsNoTracking(); } } } |
2.Durum DBContext’de select işlemi yapılırken, işaretli Entitylerde, “Deleted” alanın “false” olduğu yani silinmemiş kayıtların getirilmesi:
Aşağıda görüldüğü gibi, “BlogContex” sınıfında “OnModelCreating()” methodunda, => “modelBuilder.AddGlobalFilter()” extension’ı aşağıdaki gibi çağrılmıştır. Amaç Global Filter ile “ISoftDeletable” interface’inden türeyen sınıflarda, query yazılırken sadece =>”Deleted == false” olan satırların gelmesinin sağlanmasıdır.
Dashboard.DB/PartialEntites/BlogContext:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class BlogContext : DashboardContext { public BlogContext() { } public DbSet<CustomMenuModel> CustomMenuModel { get; set; } public BlogContext(DbContextOptions<DashboardContext> options) : base(options){} protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<CustomMenuModel>(entity => { entity.HasNoKey(); }); modelBuilder.AddGlobalFilter(); } } |
DB/Extensions/ModelBuilderExtensions:
- Aşağıda görüldüğü gibi AddGlobalFilter()’da “ISoftDeletable” interface’inden türeyen Entityler seçilir.
- ModelBuilder extension olan “SetSoftDeleteFilter()” methodu çağrılır.
- SetSoftDeleteFilter() methodu invoke edilir ve aşağıda görülen tanımlama ile filter anında, otomatik olarak “Deleted” property’si false olan kayıtlar çekilir.
-
1modelBuilder.Entity<TEntity>().HasQueryFilter(x => !x.Deleted);
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
using Dashboard.DB.PartialEntites; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; namespace Dashboard.DB.Extensions { public static class ModelBuilderExtensions { public static void AddGlobalFilter(this ModelBuilder modelBuilder) { /* Model içerisindeki tüm Entity tiplerine bak ve içerisinde ISoftDeletable olanları bul ve SetSoftDeleteFilter method'unu çağır. */ foreach (var type in modelBuilder.Model.GetEntityTypes()) { if (typeof(ISoftDeletable).IsAssignableFrom(type.ClrType)) modelBuilder.SetSoftDeleteFilter(type.ClrType); } } public static void SetSoftDeleteFilter(this ModelBuilder modelBuilder, Type entityType) { SetSoftDeleteFilterMethod.MakeGenericMethod(entityType) .Invoke(null, new object[] { modelBuilder }); } static readonly MethodInfo SetSoftDeleteFilterMethod = typeof(ModelBuilderExtensions) .GetMethods(BindingFlags.Public | BindingFlags.Static) .Single(t => t.IsGenericMethod && t.Name == "SetSoftDeleteFilter"); public static void SetSoftDeleteFilter<TEntity>(this ModelBuilder modelBuilder) where TEntity : class, ISoftDeletable { modelBuilder.Entity<TEntity>().HasQueryFilter(x => !x.Deleted); } } } |
Son olarak ilgili Entityler’in ISoftDeletable ve BaseEntity’den türetilmesi aşağıdaki gibi farklı bir partial class’da yapılmıştır.
Resim Kaynağı: https://colleeneakins.com/wp-content/uploads/2017/06/dont-get-deleted-mobile-app-feature.png
DB/PartialEntites/PartialEntites.cs: Aşağıda görüldüğü gibi DbUser “BaseEntity“‘den türetildiği için, Repository katmanında çağrılabilmiştir. Ayrıca “ISoftDeletable” interface’inden türediği için, select query’de “Deleted==true” olan kayıtlar global olarak filitrelenir ve gelmez.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using Dashboard.Core; using Dashboard.Core.Extensions; using Dashboard.DB.PartialEntites; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text; namespace Dashboard.DB.Entities { /* Veritabanında her yeni tablo oluşturulduğunda vs buraya manuel olarak eklenmesi ve BaseEntity den türetilmesi gerekmektedir. ISoftDeletable: Eklenmesi durumunda Select vb işlemlerde silinmiş olan kayıtlar gelmez. Where koşulunda IsDeleted = 0 gibi birşeye gerek kalmaz. */ class PartialEntites{} public partial class DbUser : BaseEntity, ISoftDeletable { } public partial class DbMenu : BaseEntity { } public partial class CompaniesInfo : BaseEntity { } } |
Örnek kullanımda: Sadece silinmemiş User kayıtları aşağıdaki select sorgusundan gelir.
var userList = _context.DbUser.ToList();
DB/Extensions/ISoftDeletable.cs:
1 2 3 4 5 6 7 8 9 10 11 |
using System; using System.Collections.Generic; using System.Text; namespace Dashboard.DB.PartialEntites { public interface ISoftDeletable { bool Deleted { get; set; } } } |
3.Durum DBContext’de yapılan her türlü Linq query işleminde, oluşan Sql Querylerin Output Window’da monitor edilmesi:
Öncelikle aşağıdaki paket projeye dahil edilmelidir.
Amaç Linq sorgularının sonucunda oluşan Sql Querylerin, Sql Profiler kullanılmadan, Visual Studio ortamında Output Window’da Global olarak monitor edilebilmesidir.
DB/PartialEntites/BlogContext.cs: Aşağıda görüldüğü gibi “BlogLoggerFactory” methodu ile, “LogLevel.Information” olacak şekilde OutPut Window’a oluşan Sql Query yazdırılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
using Dashboard.DB.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Dashboard.DB.PartialEntites { public class BlogContext : DashboardContext { public BlogContext() { } . . . public static readonly ILoggerFactory BlogLoggerFactory = LoggerFactory.Create(builder => { builder .AddFilter((category, level) => category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information) .AddDebug(); }); } } |
BlogContext/OnConfiguring(): Aşağıda görüldüğü gibi Debug modda, yukarıda tanımlanan “BlogLoggerFactory“, OnConfiguration’da “UseLogFactory()” methodunda çağrılmıştır. Böylece oluşan SqlQuery, Output window’a yazdırılmıştır. Release modda, performans amaçlı bu Log işlemi kapatılmıştır. Eğer istenir ise, EF 5.0 ile gelen IQueryable bir nesnenin Linq querysi sonuna, “.ToQueryString()” extension’ı konularak da oluşan SqlQuery string şeklinde alınabilir.
1 2 3 4 5 6 7 8 9 |
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { #if DEBUG base.OnConfiguring(optionsBuilder.UseLoggerFactory(BlogLoggerFactory)); #endif #if RELEASE base.OnConfiguring(optionsBuilder); #endif } |
4.Durum DBContext Entity Katmanında, SaveChanges() Methodu çağrıldığında, Reporsitory Katmanı Kullanılmadan Araya Girip Log Alma:
DB/PartialEntites/CustomSaveChangesInterceptor.cs: Burada esas amaç, bir kaydetme işlemi olmadan önce, ilgili log’un arada başka bir işleme gerek duyulmadan alınabilmesidir.
- “eventData.Context.ChangeTracker.DebugView.LongView”: Kaydedilen datanın, save işleminden önce yukarıda görüldüğü gibi önceki ve sonraki hallerine, property bazında erişilebilmektedir. Böylece, istenir ise tüm entity bazında “Audit Log” => “SaveChangesInterceptor” sınıfı sayesinde, kolaylıkla alınabilmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using System; using System.Threading; using System.Threading.Tasks; namespace Dashboard.DB.PartialEntites { public class CustomSaveChangesInterceptor : SaveChangesInterceptor { public override InterceptionResult<int> SavingChanges( DbContextEventData eventData, InterceptionResult<int> result) { Console.WriteLine($"Saving changes for {eventData.Context.Database.GetConnectionString()}"); Console.WriteLine($"Saving changes for { eventData.Context.ChangeTracker.DebugView.LongView}"); return result; } public override ValueTask<InterceptionResult<int>> SavingChangesAsync( DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = new CancellationToken()) { Console.WriteLine($"Saving changes asynchronously for {eventData.Context.Database.GetConnectionString()}"); return new ValueTask<InterceptionResult<int>>(result); } } } |
Yukarıda görülen Interceptor, “BlogContext” sınıfında aşağıda görülen “OnConfiguring()” methodunda tanımlanır.
BlogContext/OnConfiguring():
1 2 3 4 5 6 7 8 9 10 11 |
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { #if DEBUG . . base.OnConfiguring(optionsBuilder.UseLoggerFactory(BlogLoggerFactory).AddInterceptors(new CustomSaveChangesInterceptor())); #endif #if RELEASE base.OnConfiguring(optionsBuilder); #endif } |
Geldik bir makalenin daha sonuna. Bu makalede, güncel hayatta ihtiyaç duyabileceğiniz bazı durumlara karşı, DBContext katmanını nasıl ve ne amaçlı özelleştirebileceğinize değinilmiştir. Bazen Global’da yapılan bu özelleştirmeler, proje genelinde hem kod tekrarından kurtaracak, hem zaman kazandıracak hem de tekbir yerden yönetilebildiği için, test ve debug işlemlerini hızlandıracaktır.
Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Kaynaklar:
- https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-5.0/whatsnew
- https://www.learnentityframeworkcore5.com/whats-new-in-ef-core-5/keyless-entity-types
- https://www.thereformedprogrammer.net/ef-core-in-depth-soft-deleting-data-with-global-query-filters/
- https://www.pluralsight.com/courses/getting-started-entity-framework-core
Soft delete için esas bu harika:
https://www.thereformedprogrammer.net/introducing-the-efcore-softdeleteservices-library-to-automate-soft-deletes/
başka bir implementasyona gerek yok bence.
Benim örnekde SoftDelete yapmıyorum, soft delete olan kayıtları global olarak Linq sorguda getirmiyorm.
Yazılım her zaman gelişmekte olan bir sektör olduğuna göre, “bu harika başka bir implementasyona gerek yok bence.” demek çok saçma.
Sizin bu bmantığınıza göre C# 2 vs. kullanalım. C# 2 varken 3 4 vs. 9 a kadar ne gerek vardı. C# 3 çıkmadan önce de 2 harikaydı.
Makale için teşekkürler hocam.
çok işime yaradı üstat. bilgilendirme için tşkler.
Allah razı olsun.
Çok teşekkür ederim..