.Net 5 ve Yenilikleri #2

Furkan Güngör
Devops Türkiye☁️ 🐧 🐳 ☸️
9 min readNov 22, 2020

--

Aylar önce .Net 5 ile ilgili gelen yenilikleri kaleme almıştım. O zamanlar sadece iki adet preview yayınlanmıştı. 🤔 Zaman ne kadar hızlı değil mi? 🤔
Geçtiğimiz günlerde .Net 5.0 yayınlandı ve büyük release gerçekleşti. 😀
Bu yazıda, yayınlanan bu sürümdeki gelen yenilikleri, performans iyileştirmelerini ve gelecek sürümler için nasıl planlamalar yapıldığını inceleyeceğiz.

Serinin bir önceki yazısını sizler için buraya bırakıyorum.

.Net 5 ile birlikte ASP.NET Core, Entity Framework Core, C# 9, gRPC ve SignalR gibi teklonojiler de güncellemeler aldı. Evet biraz uzun bir yazı olacak. 😉 Hadi başlayalım. 🛫

ASP.NET Core 5.0

Eklenen yeni özelliklerden bahsederken aşağıda listeleyeceğim adımları uygulamaya özen göstereceğim.

  • Tanım?
  • Neden ihtiyaç duyuldu?
  • Bu özellik eklenmeden önce geliştiriciler bu işlemleri nasıl gerçekleştiriyordu?
  • Eklenen bu özellik nasıl kullanılır?

DateTime Model Binding (MVC and Razor)

Model Bindingartık DateTime için UTCzaman dilimlerini destekliyor. Gelen request içerisinde UTCzaman dilimi içeriyorsa Model Bindingonu ilgili UTCaralığına bağlayacaktır.

ASP.NET Core 5.0 sürümünden önce; kullanıcı request içerisinde UTCzaman dilimini gönderdiğinde Model Bindingbunu UTC DateTimenesnesi yerine Local bir DateTimenesnesine dönüştürüyordu.

Geliştiriciler kullanıcının request içerisinde gönderdiği UTCzaman dilimine ulaşmak için Custom Model Binder yazarak bu problemi çözüyorlardı.

Biraz kod yazalım. ⌨

DateTimeController.cs

Request atalım ve sonucu görelim.

https://localhost:44393/time?time=2020-11-17T02:30:04.0576719Z

Current culture: en-US
Raw querystring value is: 2020-11-17T02:30:04.0576719Z
Model-bound value is: 2020-11-17T19:30:04.0576719-07:00, Kind is: Local

Custom Model Binder yazdıktan sonra tekrar request atıp sonucu izleyelim.

Current culture: en-US
Raw querystring value is: 2020-11-17T02:30:04.0576719Z
Model-bound value is: 2020-11-17T02:30:04.0576719Z, Kind is: UTC

ASP.NET Core 5.0 ile Custom Model Binder yazmadan UTC DateTime Model Binder tarafından desteklenir. Artık bu kodları projenizden kaldırabilirsiniz🔥

Model Binding with Record Type(Mvc and Razor, C# 9)

Record türlerini C# 9 kısmında anlatmayı düşünüyordum ancak Model Binding desteği olduğu için kısa bir tanımını bu bölümde yapacağım.

Syntax olarak sınıflara oldukça benzer gözüken bu yapı aslında temelde çok farklı bir felsefe barındırıyor. Record yapısını sınıflardan ayıran en önemli özelliği tanımlandığı nesneyiValue Type ve Immutableyapmasıdır.

Record türlerini aşağıdaki gibi kullanarak Model Binding ve Validation işlemlerini gerçekleştirebilirsiniz.

Open API Supports by Default (Web API)

Geliştirdiğimiz servislerde olmazsa olmaz özelliklerden biri olan Open API desteğini ASP.NET Core 5.0 sürümünden önce kendimiz uygulamak zorundaydık. ASP.NET Core 5.0 ile default olarak Open API desteği eklendi. Böylece projemizin ilk aşamasında daha öğretici bir şablon ile karşılaşıyoruz.

Projenizi açtığınızda Swashbuckle.AspNetCore paketinin uygulamanıza eklendiğini göreceksiniz.

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
</ItemGroup>

Ayrıca Startup.cs içerisinde Open API desteğinin nasıl konfigüre edildiğini görebilirsiniz.

public void ConfigureServices(IServiceCollection services)
{

services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApp1", Version = "v1" });
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
"WebApp1 v1"));
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}

Eğer oluşturduğunuz projelerde Open API desteğinin default olarak eklenmesini istemiyorsanız aşağıda CLI komutunu kullanabilirsiniz.

dotnet new webapi --no-openapi true

Blazor WebAssembly Performans İyileştirmeleri

UI Rendering ve Json Serialization aşamalarında, farklı senaryolarda ortalama 3 kat daha performanslı çalışabilecek performans iyileştirmeleri yer alıyor.

Blazor File Input

Blazor File Input ile dosya yükleme işlemleri gerçekleştirebilirsiniz. HTML tarafında alışık olduğumuz file etiketini render eden bu component tarayıcıda bulunan .Net kodunu çalıştırır ve OnChange olayını tetikler.

<InputFile OnChange="OnInputFileChange" multiple />

<div class="image-list">
@foreach (var imageDataUrl in imageDataUrls)
{
<img src="@imageDataUrl" />
}
</div>

@code {
IList<string> imageDataUrls = new List<string>();

async Task OnInputFileChange(InputFileChangeEventArgs e)
{
var imageFiles = e.GetMultipleFiles();

var format = "image/png";
foreach (var imageFile in imageFiles)
{
var resizedImageFile = await imageFile.RequestImageFileAsync(format, 100, 100);
var buffer = new byte[resizedImageFile.Size];
await resizedImageFile.OpenReadStream().ReadAsync(buffer);
var imageDataUrl = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
imageDataUrls.Add(imageDataUrl);
}
}
}

gRPC

.Net dünyasında .Net Core 3.0 versiyonundan itibaren gittikçe popülerliğini artıran gRPC, .Net 5 ile birlikte hayatımızdan çıkan teklonojiler arasında yer alan WCFin yerini dolduruyor.

gRPC teklonojisi ile ilgili sade bir başlangıç yapmak istiyorsanız hemen tıklayın. 😉

gRPC ile yaptığım çalışmaları ve Microservice mimarisinde haberleşme tekniği olarak nasıl kullandığımı merak ediyorsanız, aylar önce yazdığım bu yazıyı okumanızı tavsiye ederim.

.Net 5 ile birlikte gRPC için oldukça büyük performans iyileştirmeleri yapıldı.

SignalR Paralel Hub

ASP.NET Core 5.0 ile birlikte SignalR artık paralel hub çağrılarına izin verilir. Startup.cs içerisinde yapacağınız değişiklikle default olarak istemcinin birden fazla hub çağırmasına izin verebilirsiniz.

public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR(options =>
{
options.MaximumParallelInvocationsPerClient = 5;
});
}

Custom Endpoint Authentication

Belirli bir deseni olan veya istediğiniz herhangi bir endpointi gelen requestlerin authentication aşamasına uğramadan geçmesi için ne yaparsanız?

Akla gelen ilk yöntem Controller üzerine [AllowAnonymous] eklemektir. ASP.NET Core 5.0 sürümü ile Startup.cs içerisinde istediğiniz endpointi AllowAnonymous yapabilirsiniz.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
})
.AllowAnonymous();
});
}

Dotnet Watch

ASP.NET Core 5.0 sürümü ile komut satırında dotnet watch komutunu çalıştırdığınızda varsayılan tarayıcı üzerinde projeniz açılır ve kodda yaptığınız değişiklikler otomatik olarak tarayıcınıza yansır böylece daha hızlı bir geliştirme ortamına sahip olursunuz.

Entity Framework Core 5.0

.NET 5 ile birlikte EF Core 5.0 sürümü de yayınlandı. Yazının bu aşamasında gelen yeni özellikleri ve bu özelliklerin bizlere sağladı avantajlar üzerinde konuşacağız.

Many-to-Many Mapping

İlişkisel veri tabanları için One-to-One One-to-Many Many-to-Many gibi farklı ilişki yöntemleri vardır. Bu ilişkiler sayesinde varlıklar arasında nasıl bir iletişim olduğunu belirleriz. Eğer bu ilişkiler iyi bir şekilde belirlenmezse projelerimiz kısa zamanda gelen yeni isteklere cevap veremez hale gelebiliyor.
Eğer yeni bir projeye başlıyorsak ve mevcut bir veri tabanımız yoksa genellikle CodeFirst felsefesi ile veri tabanı modellerimizi belirleyip DbContext içerisinde bu modellerin birbirleri ile olan ilişkilerini tanımlıyoruz.

Gelen bu özellik sayesinde Many-to-Many ilişkilerinde DbContext içerisinde detaylı tanımlama yapmamıza gerek kalmıyor.

public class Post
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
public int Id { get; set; }
public string Text { get; set; }
public ICollection<Post> Posts { get; set; }
}

Post ve Tag varlıkları arasında Many-to-Many ilişkisi olduğunu görüyoruz. DbContext içerisinde modellerimizi tanımlayalım.

public class BlogContext : DbContext
{
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
}

Normalde bu adımdan sonra yapılması gereken OnModelCreating içerisinde bu iki model arasındaki ilişkiyi tanımlamaktı. Ancak yeni sürüm ile bu tanımlamayı yapmadan migration oluşturabilirsiniz.

Index Attribute

Sorgularınızin response süresinden memnun değil misiniz? O zaman üreteceğiniz çözümler arasında ilgili tabloya atılan sorguları analiz edip gerekli alanlar için index tanımlamak olacaktır. EF Core 5.0 sürümünden önce DbContext içerisinde index tanımlamaları yapabiliyorduk. Daha kolay bir yöntem olan IndexAttribute aşağıdaki gibi kullanılıyor.

[Index(nameof(FirstName), nameof(LastName), IsUnique = true)]
public class User
{
public int Id { get; set; }

[MaxLength(64)]
public string FirstName { get; set; }

[MaxLength(64)]
public string LastName { get; set; }
}

Filtered Include

EF Core 5.0 Preview 3 sürümünde aramıza katılan bu özellik zamanında ilgimi çekmişti ve bu özelliği içeren bir performans testi gerçekleştirmiştim. Merak ettiniz mi? O zaman buraya tıklayabilirsin. 🖱

EF Core 5.0 sürümü ile artık Include içerisine query yazabilirsiniz.

var blogs = context.Blogs
.Include(e => e.Posts.Where(p => p.Title.Contains("Cheese")))
.ToList();

Simple Logging

Uygulamaları geliştirirken alınan hataları, uyarıları kaydetme ve sonrasında kaydettiğimiz bu kayıtlardan sonuçlar çıkartırız. Evet kısaca log tutarız. 😊
EF Core 5.0 ile birlikte üretilen SQL sorgularını, hataları görmek daha kolay. Bu sürümden önce mecburen Microsoft.Extensions.Logging.Debug paketini indirip aşağıdaki konfigürasyonları yapmak gerekiyordu.

EF Core 5.0 sürümü ile;

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.LogTo(Console.WriteLine);

Daha hassas veriler için;

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.LogTo(Console.WriteLine)
.EnableSensitiveDataLogging();

LogLevel seçebilirsiniz;

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);

Exclude Tables from Migration

Bazı senaryolarda belirleyeceğiniz modellerin Migration aşamasına uğramamasını isteyebilirsiniz.

public class AuthorizationContext : DbContext
{
public DbSet<User> Users { get; set; }
}

public class ReportingContext : DbContext
{
public DbSet<User> Users { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().ToTable("Users", t => t.ExcludeFromMigrations());
}
}

Split Query

Benim için en önemli özelliklerden biri Split Query EF Core 3.0 sürümü ile karşılaştıralım. EF Core 3.0 sürümünde her bir LINQ sorgusu için tek SQL sorgusu oluşturur ancak EF Core 5.0 sürümünde her bir LINQ sorgusu için birden fazla SQL sorgusu üretibilir. Peki birden fazla SQL sorgusunun üretilmesi nasıl bir avantaj sağlayacak?🤔
Ağır sorgularda özellikle Join kullandığımız sorgularda kaç tane Join komutunuz varsa her biri farklı bir çekirdekte çalışmak isteyecektir. Kaynaklar o an yetersiz ise sorgunuzun response süresi yükselecektir. Ancak tek bir SQL sorgusu yerine birden fazla SQL sorgusu üretip ayrı ayrı çalıştırdıktan sonra alınan cevapları merge etmek şüphesiz ağır sorgularda avantajlı olacaktır.

Basit bir Include işlemi ile üretilen SQL sorgularını karşılaştıralım.

var artists = context.Artists
.Include(e => e.Albums).ThenInclude(e => e.Tags)
.ToList();
--------------------------------------------------------------------SELECT "a"."Id", "a"."Name", "t0"."Id", "t0"."ArtistId", "t0"."Title", "t0"."Id0", "t0"."AlbumId", "t0"."Name"
FROM "Artists" AS "a"
LEFT JOIN (
SELECT "a0"."Id", "a0"."ArtistId", "a0"."Title", "t"."Id" AS "Id0", "t"."AlbumId", "t"."Name"
FROM "Album" AS "a0"
LEFT JOIN "Tag" AS "t" ON "a0"."Id" = "t"."AlbumId"
) AS "t0" ON "a"."Id" = "t0"."ArtistId"
ORDER BY "a"."Id", "t0"."Id", "t0"."Id0"

AsSplitQuery kullanarak Split Query özelliğini kullanabilirsiniz.

var artists = context.Artists
.AsSplitQuery()
.Include(e => e.Albums).ThenInclude(e => e.Tags)
.ToList();
--------------------------------------------------------------------
SELECT "a"."Id", "a"."Name"
FROM "Artists" AS "a"
ORDER BY "a"."Id"

SELECT "a0"."Id", "a0"."ArtistId", "a0"."Title", "a"."Id"
FROM "Artists" AS "a"
INNER JOIN "Album" AS "a0" ON "a"."Id" = "a0"."ArtistId"
ORDER BY "a"."Id", "a0"."Id"

SELECT "t"."Id", "t"."AlbumId", "t"."Name", "a"."Id", "a0"."Id"
FROM "Artists" AS "a"
INNER JOIN "Album" AS "a0" ON "a"."Id" = "a0"."ArtistId"
INNER JOIN "Tag" AS "t" ON "a0"."Id" = "t"."AlbumId"
ORDER BY "a"."Id", "a0"."Id"

SaveChanges Interception

SaveChanges metodu track edilen her varlığın durumunu günceller. EF Core 5.0 sürümünden önce DbCommandInterceptor yazabiliyorduk. EF Core 5.0 sürümü ile artık SaveChanges için Interceptor yazabilirsiniz. Interceptor sayesinde gerçekleşen olaylara müdahele edebilirsiniz. Örneğin; SaveChanges için bir interceptor yazarak küçük bir AuditLog özelliği geliştirebilirsiniz.

public class MySaveChangesInterceptor : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
Console.WriteLine($"Saving changes for {eventData.Context.Database.GetConnectionString()}");

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);
}
}

DbContext initilaze edilirken AddInterceptor ile eklemeyi unutmayın.

services.AddDbContext(b => b.UseSqlServer(connectionString)  .AddInterceptors(new MySaveChangesInterceptor()));

ChangeTracker.Clear

Bir önceki maddede SaveChanges özelliğini anlatırken modellerin track edildiğinden bahsetmiştik. ChangeTracker.Clear ile track edilen tüm modellerin durumunu temizleyebilirsiniz.

📝 Her taraf sorgularla doldu.😊 Peki bu sorguların response süreleri ne kadar?🤔 Sistem üzerinde en yavaş endpoint kim?🤔 Merak ediyorsanız sizler için bir kütüphane geliştirdim. Bu soruların hepsinin cevabı burada. 🛫

C# 9

.NET 5 ile birlikte C# yeni sürümüne ulaştı. Yazının bu aşamasında C# 9 ile birlikte hayatımıza giren özellikleri inceleyeceğiz.

Init-Only Properties

Bazı senaryolarda nesneleriniz yaratılırken minimum parametrelere ihtiyaç duyarsınız. Yani nesneniz oluşturulurken o parametrelerin kesinlikle bilinmesi gerekiyordur. Init ile işaretlenmiş property readonlyolur.

public class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
--------------------------------------------------------------------
Person person = new Person(){FirstName="Furkan", LastName="Gungor"};

Target-typed new expressions

Tip belli ise neden new sonrasında tipini tekrar yazalım?🤔
Point p = new (3, 5);

Records

En çok ses getiren özelliklerden biri.
Tanım : Record içerisinde data saklayan bir yapıdır. Record syntax açısından class ile oldukça benzer ancak en temelde çok farklı özelliklere sahip olan recordlar, data yönetimi ile classlardan ayrılıyor.

Eğer bir nesnenizi record olarak tanımlarsanız o nesneyi value type yapmış olursunuz. Yani iki record arasında eşitlik gibi işlemler yapmaya çalıştığınızda nesnelerin referanslarına değil, içerdiği datalara göre bakılacaktır.

With Expressions

Bir nesnenin bazı parametrelerini değiştirip kalan kısmını kopyalayarak yeniden oluşturmanızı sağlayan bir keyword.

public record Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
--------------------------------------------------------------------
Person person = new Person(){FirstName="Furkan", LastName="Gungor"};
Person person2 = person with {FirstName="Ali"};
Console.WriteLine($"{person2.FirstName}\t{person2.LastName}");Result;
Ali Gungor

Parameter Null Checking

Parametrenin null olması.😢 Sürekli o verinin null olup olmadığı kontrol edilip sonra işlemleri yapmak gerek. C# 9 ile bu işi biraz daha basit hale getirdiler.

public void SetCustomerName(string customerName)
{
if(string.IsNullOrEmpty(customerName))
throw new ArgumentNullException(nameof(Customer));
/// Other codes.
}
With C# 9
public void SetCustomerName(string customerName!) => throw new ArgumentNullException(nameof(Customer));
{
/// Other codes.
}

--

--

Furkan Güngör
Devops Türkiye☁️ 🐧 🐳 ☸️

Solution Developer — I want to change the world, give me the source code.