ASP.NET Core 6/Entity Framework Core Kullanımı

Entity Framework Core Microsoft'un sunduğu bir ORM (object-relational mapping) çözümüdür. Veritabanında ilişkisel olarak tutulan verilerin .NET ortamında .NET nesneleriyle nesne tabanlı olarak görülebilmesini, bu nesneler üzerinde değişiklik yapılabilmesini, depoya yeni nesne eklenebilmesini ve sonucun tekrar ilişkisel veritabanına yazılmasını sağlar. Entity Framework Core sayesinde büyük ölçüde veritabanına direkt SQL gönderme zahmetinden kurtulmuş oluruz. Ancak Entity Framework Core bizi SQL bilme zorunluluğundan kurtarmamaktadır. Sadece kodun daha güzel, düzenli, basit ve sade olmasını sağlamakta ve dahili .NET nesneleriyle veritabanı arasında geçişin kolay olmasını sağlamaktadır. Direkt veritabanına SQL yazdığımız durumda veritabanına sıkı bir şekilde bağlı oluyorduk. Entity Framework Core bizi bu sıkı bağdan kurtarmaktadır ve dahili .NET nesnelerinden veritabnına geçişi tek satır kod değişimine indirgemektedir.

Bazen Entity Framework Core LINQ sorgularını güzel bir şekilde SQL'e dönüştüremeyebilir. Çünkü birisi nesne tabanlı, birisi ilişkisel çalışmaktadır. Her İngilizce kelimenin bir Türkçe karşılığı olmadığı gibi bazı LINQ sorgularının basit bir SQL karşılığı yoktur. Bazen LINQ kötü SQL sorgusuna dönüştürülmekte, dönüştürülen SQL sorgusu önce çok fazla verinin çekilmesini, sonra bu verilerin büyük kısmının atılmasını gerektirmektedir. İşte böyle bir durumda üretilen SQL sorgusunu görme ve müdahale etme imkanımız vardır. Bunun için tabii ki çok iyi SQL bilmek zorundayız.

Şimdi "ASP.NET Core Empty" şablonuyla yeni bir proje oluşturalım. Projeye Veritabani ismini verelim.

Entity Framework Core'un kurulumu

değiştir

Öncelikle Entity Framework Core araç paketini kurmamız gerekiyor. Model sınıfından veritabanı şemasına dönüşüm (migration) araç paketi aracılığıyla olmaktadır. Araç paketini kurmak için "Geliştirici PowerShell" pencereciğinde .csproj dosyasının olduğu klasöre geçin ve aşağıdaki iki komutu verin:

dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef --version 6.0.0

İlk komut ilgili araç paketi varsa silmekte, ikinci komut araç paketinin 6.0.0 versiyonunu kurmaktadır. Araç paketini kurduktan sonra NuGet paket yöneticisini kullanarak Microsoft.EntityFrameworkCore.Design ve Microsoft.EntityFrameworkCore.SqlServer paketlerinin 6.0.0 versiyonlarını kuralım.

Model sınıfının oluşturulması

değiştir

Şimdi projenizde Models isimli bir klasör oluşturun, içinde Ogrenci.cs isminde bir sınıf oluşturun, sınıfın içeriği şöyle olsun:

namespace Veritabani.Models
{
    public class Ogrenci
    {
        public long Id { get; set; }
        public string Ad { get; set; }
        public string Soyad { get; set; }
    }
}

Kuşkusuz daha karmaşık model sınıfları da olabilir. Ben Entity Framework Core'a odaklanmak istediğim için olabilecek en basit model sınıfını seçtim.

Bağlam sınıfının oluşturulması

değiştir

Artık elimizde modelimiz var. Şimdi bu modeli veritabanına bağlayacak olan sınıfı yazmaya sıra geldi. Şimdi Models klasöründe OgrenciContext.cs isimli bir sınıf oluşturun ve sınıfın içeriği şöyle olsun:

using Microsoft.EntityFrameworkCore;
namespace Veritabani.Models
{
    public class OgrenciContext:DbContext
    {
        public OgrenciContext(DbContextOptions<OgrenciContext> opts) : base(opts) { }
        public DbSet<Ogrenci> Ogrenciler => Set<Ogrenci>();
    }
}

Sınıfımız model sınıfı ile veritabanı arasında köprü görevi görecektir. Biz .NET nesneleri üzerinde modifikasyon yapıp depoya yazdığımızda bu sınıf, sonuçları SQL'e dönüştürüp veritabanına gönderecektir. Sınıfımız DbContext sınıfından türetilmiştir. Yapıcı metot DbContextOptions<OgrenciContext> tipinde tek bir parametre almakta ve bu parametreyi olduğu gibi ana sınıfın yapıcı metoduna göndermektedir. Şimdilik sınıfımızda depodaki bütün öğrencileri döndürmeye yarayan Ogrenciler isimli bir özellik vardır. İleride kendimiz de Ogrenciler tablosunda seçim yapan ve Ogrenciler tablosunda değişiklik yapan çeşitli özellik ve metotları bu sınıfa ekleyebileceğiz.

Veritabanı servisinin yapılandırılması

değiştir

Şimdi veritabanına erişim için servis ekleyerek bağlam sınıfını yapılandırmaya sıra geldi. Şimdi Program.cs dosyamızı şöyle değiştirelim:

using Microsoft.EntityFrameworkCore;
using Veritabani.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<OgrenciContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:OgrenciConnection"]);
});
var app = builder.Build();
app.MapGet("/", async context => {
    await context.Response.WriteAsync("Hello World!");
});
app.Run();

Burada Entity Framework Core için bir servis tanımlaması yapılmaktadır. Burada veritabanına bağlantı için kullanılacak köprü sınıfının OgrenciContext olduğunu, veritabanının Sql Server olduğunu ve veritabanına bağlanmak için kullanılacak connection string'i belirttik. Şimdi appsettings.json dosyasında connection string'i tanımlamaya sıra geldi:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "OgrenciConnection": "Server=DESKTOP-J5SDIAM;Database=Ogrenci;Trusted_Connection=True;"
  }
}

Burada Microsoft.EntityFrameworkCore kategorisinin log seviyesini Information'a çektik. Bu sayede LINQ'ten SQL'e dönüştürülen sorguları sunucu konsolunda görebileceğiz. Bu aşamada veritabanında Ogrenci diye bir veritabanı yok, ancak oluşturmaya da gerek yok. Migrate işlemi veritabanını ve tabloyu bizim yerimize oluşturacak.

Migration'ın oluşturulup uygulanması

değiştir

Şimdi ASP.NET Core tarafında gerekli olan her şeyi tamamladık. Şimdi bir tuşa basıp bu işlemleri veritabanına uygulamaya sıra geldi. .NET tarafında mantıksal olarak tanımlanan sınıfların veritabanı ortamında fiziksel veritabanı ve tablolara dönüştürülmesine Migration (göç) denir. Migrate işlemi dersin başında yüklediğimiz araç komutu ile yapılır. Migrate işlemi ilk aşamada .NET sınıflarından veritabanı tablolarına dönüşümü için veya ileriki aşamalarında model sınıflarında yapılan değişikliklerin veritabanı şemasına yansıtılması için kullanılır. Şimdi projede bir hata olup olmadığını görmek için solution'ı bir kere build edin. Hata yoksa "Geliştirici PowerShell" pencereciğinde .csproj dosyanızın olduğu klasöre geçin ve eşağıdaki komutu verin:

dotnet ef migrations add Initial

Bu aşamada henüz veritabanına bir şey yazılmadı. Sadece veritabanına yazma yapacak .NET sınıfı oluşturuldu. Bu sınıfın Migrations klasöründe ve <zaman_damgası>_Initial.cs isminde olması gerekiyor. Initial ilk migration'a verilen geleneksel isimdir. Veritabanında yapılacak işlemleri görmek için ilgili Initial dosyasının içine bakabilirsiniz. Şimdi migration'ı uygulamanın zamanı geldi:

dotnet ef database update

Bu komut migration sınıfındaki komutları çalıştırmaktadır. Komutun çalıştığını SQL Server Management Studio'dan teyit edebilrsiniz. Ayrıca Microsoft.EntityFrameworkCore kategorisi için log seviyesini Information olarak ayarladığımız için komut girdiğiniz "Geliştirici PowerShell" pencereceğinde de çalıştırılan SQL komutlarını görebilirsiniz.

Veritabanına verilerin yüklenmesi

değiştir

Elimizde bir veritabanı ve bu veritabanının içinde bir tablo var, ancak tablonun işi boş. Şimdi bu tabloyu doldurmaya sıra geldi. Şimdi projenizin Models klsörüne SeedOgrenci.cs isimli bir dosya ekleyin. Dosyanın içeriği şöyle olsun:

using Microsoft.EntityFrameworkCore;
namespace Veritabani.Models
{
    public class SeedOgrenci
    {
        private OgrenciContext context;
        public SeedOgrenci(OgrenciContext dataContext)
        {
            context = dataContext;
        }
        public void SeedDatabase()
        {
            context.Database.Migrate();
            if (context.Ogrenciler.Count() == 0)
            {
                List<Ogrenci> ogrenciler = new List<Ogrenci>
                {
                    new Ogrenci { Ad = "Bekir", Soyad = "Oflaz" },
                    new Ogrenci { Ad = "Hatice", Soyad = "Özdoğan" },
                    new Ogrenci { Ad = "Hakan", Soyad = "Gürel" },
                    new Ogrenci { Ad = "Hayriye", Soyad = "Aktürk" },
                    new Ogrenci { Ad = "Hande", Soyad = "Şanal Pala" }
                };
                context.Ogrenciler.AddRange(ogrenciler);
                context.SaveChanges();
            }
        }
    }
}

Bu kodda öncelikle bekleyen bir Migration olup olmadığını kontrol ediyoruz. Aslında bekleyen bir migration olmadığını biliyoruz. Çünkü biraz önce dotnet ef database update kodunu uyguladık. Ancak işimizi garanti alrına almak için bekleyen bir migration olup olmadığını kontrol ediyoruz. Eğer bekleyen bir migration varsa Migrate işlemi yapıyoruz.

Eğer veritabanında hiç öğrenci yoksa öğrenci nesnelerini oluşturup veritabanına ekliyoruz. context üzerinde çalışan SaveChanges() metoduyla context üzerindeki .NET nesnelerinde yapılan değişimin veritabanına yansıtılmasını sağlıyoruz. SeedOgrenci sınıfının kullanılmasını sağlamak için Program.cs dosyamızı şöyle değiştirmeliyiz:

using Microsoft.EntityFrameworkCore;
using Veritabani.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<OgrenciContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:OgrenciConnection"]);
});
builder.Services.AddTransient<SeedOgrenci>();
var app = builder.Build();
app.MapGet("/", async context => {
    await context.Response.WriteAsync("Hello World!");
});
bool cmdLineInit = (app.Configuration["INITDB"] ?? "false") == "true";
if (app.Environment.IsDevelopment() || cmdLineInit)
{
    var seedData = app.Services.CreateScope().ServiceProvider.GetRequiredService<SeedOgrenci>();
    seedData.SeedDatabase();
}
if (!cmdLineInit)
{
    app.Run();
}

Bu kod biraz karmaşık gibi gözüküyor, ama aslında karmaşık değil. Öncelikle

builder.Services.AddDbContext<OgrenciContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:OgrenciConnection"]);
});

kodlarıyla OgrenciContext sınıfına servis aboneliği yapıyoruz, daha doğrusu OgrenciContext sınıfını servis olarak kullanmak istediğimizi belirtiyoruz. Artık nerede bir sınıf yapıcı metot yoluyla bir OgrenciContext nesnesi istese, ilgili sınıf nesnesi ASP.NET Core tarafından oluşturuluyorsa OgrenciContext nesnesi otomatik olarak enjekte edilecek.

builder.Services.AddTransient<SeedOgrenci>();

Burada da SeedOgrenci sınıfını servis olarak kullanıyoruz. SeedOgrenci servisi de yapıcı metot yoluyla OgrenciContext servisine bağlı. Biz bir SeedOgrenci nesnesi istediğimizde çözümlemeler zincir şeklinde otomatik yapılacak.

bool cmdLineInit = (app.Configuration["INITDB"] ?? "false") == "true";
if (app.Environment.IsDevelopment() || cmdLineInit)
{
    var seedData = app.Services.CreateScope().ServiceProvider.GetRequiredService<SeedOgrenci>();
    seedData.SeedDatabase();
}
if (!cmdLineInit)
{
    app.Run();
}

Burası biraz kafanızı karıştırabilir. Ancak aslında son derece basittir. Burada eğer uygulama Development sürecinde değilse (Production veya Staging sürecinde) ve INITDB komut satırı argümanının değeri true değilse veya böyle bir komut satırı argümanı hiç yoksa veritabanı seed'lenmemektedir. Diğer bütün durumlarda veritabanı seed'lenmektedir. Uygulamamız varsayılan durumda Development aşamasında olacağı için her halükarda seed'lenecektir. Eğer Production aşamasında olsaydı seed'lenmesinin tek yolu INITDB komut satırı argümanının değerinin true olması olacaktı. İkinci if'te eğer INITDB komut satırı true değilse veya hiç yoksa sunucuyu çalıştır, aksi halde çalıştırma diyoruz.

NOT: Bu kodumuzda daha önce Servisler ve Dependecy Injection dersinde gördüğümüz bir tekniği uyguladık, app değişkeni üzerinden bir transient veya scoped servise eriştik. Varsayılan durumda app değişkeni üzerinden transient veya scoped servislere erişemezsiniz. app değişkeni üzerinden bir transient veya scoped servise erişmek için öncelikle CreateScope() metoduyla scope oluşturduk, daha sonra oluşan bu nesne üzerinden servise eriştik.

Bazen sunucuyu çalıştırmadan veritabanını veri ile doldurmak isteyebiliriz. Bu durumda Geliştirici PowerShell pencereciğinde aşağıdaki komutu veririz:

dotnet run INITB=true

Bu komut uygulamamız hangi geliştirme sürecinde olursa olsun çalışacaktır. Eğer veritabanını silmek istiyorsanız Geliştirici PowerShell pencereciğinde

dotnet ef database drop --force

komutunu çalıştırabilirsiniz. Bu aşamadan sonra tekrar veritabanını, tabloyu ve tablodaki verileri oluşturmak istiyorsanız aşağıdaki komutu vermeniz yeterlidir:

dotnet run INITDB=true

Bu komut Initial migration'ındaki komutları uygulayacak ve veritanındaki tabloyu verilerle dolduracaktır.

Verilerin bir endpoint'te kullanılması

değiştir

Veritabanını ve tabloyu oluşturup örnek verileri veritabanına yükledikten sonra sıra, veritabanındaki verilere bir endpoint'ten erişmeye geldi. Şimdi projemizin ana klasörüne OgrenciEndpoint.cs isminde bir endpoint sınıfı ekleyelim. İçeriği şöyle olsun:

using Veritabani.Models;
namespace Veritabani
{
    public class OgrenciEndpoint
    {
        public async Task Endpoint(HttpContext context)
        {
            OgrenciContext oc = context.RequestServices.GetRequiredService<OgrenciContext>();
            List<Ogrenci> ogrenciler = oc.Ogrenciler.ToList();
            foreach(var v in ogrenciler)
            {
                await context.Response.WriteAsync(v.Id + " " + v.Ad + " " + v.Soyad + "\n");
            }
            Ogrenci haydar = new Ogrenci { Ad = "Haydar", Soyad = "Yılmaz" };
            oc.Ogrenciler.Add(haydar);
            await oc.SaveChangesAsync();
        }
    }
}

Şimdi bu endpoint'i rota yapılandırmasına ekleyelim. Kolaylık olması açısından bu endpoint'i / path'ına ekleyeceğim:

using Microsoft.EntityFrameworkCore;
using Veritabani.Models;
using Veritabani;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<OgrenciContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:OgrenciConnection"]);
});
builder.Services.AddTransient<SeedOgrenci>();
var app = builder.Build();
app.MapGet("/", new OgrenciEndpoint().Endpoint);
bool cmdLineInit = (app.Configuration["INITDB"] ?? "false") == "true";
if (app.Environment.IsDevelopment() || cmdLineInit)
{
    var seedData = app.Services.CreateScope().ServiceProvider.GetRequiredService<SeedOgrenci>();
    seedData.SeedDatabase();
}
if (!cmdLineInit)
{
    app.Run();
}

Endpoint sınıfımız basitçe veritabanındaki tüm öğrencileri çekmekte, bunların bilgilerini ekrana yazmakta, daha sonra Haydar isimli bir öğrenciyi veritabanına eklemektedir.

Hassas veri loglamayı etkinleştirme

değiştir

Entity Framework Core'un yaptığı her SQL sorgusunu sunucunun konsol ekranında görebilmekteyiz. Ancak gizliliği korumak adına Entity Framework Core SQL sorgularındaki parametreleri [Parameters=[@p0='?' (Size = 4000), @p1='?' (Size = 4000)] şeklinde gizlemektedir, parametreler yerine soru işareti koymaktadır. Konsol ekranına gönderilen bu loglarda parametre değerlerini görebilmek için Program.cs dosyasındaki AddDbContext<OgrenciContext> servis çağrısını şöyle değiştirebilirisiniz:

builder.Services.AddDbContext<OgrenciContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:OgrenciConnection"]);
    opts.EnableSensitiveDataLogging(true);
});