.Net 9 ve Yenilikleri

--

.Net 9 sürümü 12 Kasım tarihinde yayınlandı. .Net 9 ile birlikte MAUI, Entity Framework Core, Asp.Net Core gibi ekosistemin diğer ürünleri de yayınlanan sürüm ile yeni özellikler, performans iyileştirmeleri gibi güncellemeler aldı. Bu yazıda .Net 9 ile hayatımıza giren yenilikleri, performans iyileştirmelerini ve bunların kullanımını anlatmaya çalışacağım.

Hadi başlayalım. 🧐

Performans

Bu sürüm performans odaklı çok fazla yenilik taşıyor. Yukarıdaki grafikte göründüğü gibi;

  • %15 Daha Yüksek RPS (Request Per Second)
  • %93 Daha Az Bellek Kullanımı

TechEmpower benchmark testleri ile kanıtlanmıştır. Özellikle memory tarafındaki dramatik düşüş Server GC’deki değişikliklerin bir sonucu. GC üzerinde yapılan bu iyileştirmeler ile uygulamanızın çalıştığı ortamın ve yükün dinamik olarak analiz edilerek GC davranışlarını optimize eder. Özellikle sunucu tarafında yüksek trafikli uygulamalarda performansı arttırmayı hedefler. Bu yaklaşım, çalışma zamanında uygulamanın bellek kullanımı ve yük durumuna göre GC ayarlarını uyarlayarak daha dengeli bir bellek yönetimi sunar.

Bunun yanı sıra .Net 9 JIT ve PGO üzerinde de önemli iyileştirmeler yapılmıştır. Bu geliştirmeler ile performans artışları ve daha verimli kod üretimine odaklanmıştır.

LINQ

// dotnet run -c Release -f net8.0 --filter "*" --runtimes net8.0 net9.0

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);

[MemoryDiagnoser(false)]
[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
public class Tests
{
private IEnumerable<int> _arrayDistinct = Enumerable.Range(0, 1000).ToArray().Distinct();
private IEnumerable<int> _appendSelect = Enumerable.Range(0, 1000).ToArray().Append(42).Select(i => i * 2);
private IEnumerable<int> _rangeReverse = Enumerable.Range(0, 1000).Reverse();
private IEnumerable<int> _listDefaultIfEmptySelect = Enumerable.Range(0, 1000).ToList().DefaultIfEmpty().Select(i => i * 2);
private IEnumerable<int> _listSkipTake = Enumerable.Range(0, 1000).ToList().Skip(500).Take(100);
private IEnumerable<int> _rangeUnion = Enumerable.Range(0, 1000).Union(Enumerable.Range(500, 1000));

[Benchmark] public int DistinctFirst() => _arrayDistinct.First();
[Benchmark] public int AppendSelectLast() => _appendSelect.Last();
[Benchmark] public int RangeReverseCount() => _rangeReverse.Count();
[Benchmark] public int DefaultIfEmptySelectElementAt() => _listDefaultIfEmptySelect.ElementAt(999);
[Benchmark] public int ListSkipTakeElementAt() => _listSkipTake.ElementAt(99);
[Benchmark] public int RangeUnionFirst() => _rangeUnion.First();
}

JSON

// dotnet run -c Release -f net8.0 --filter "*" --runtimes net8.0 net9.0

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Text.Json;
using System.Reflection;
using System.Text.Json.Serialization;

BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);

[MemoryDiagnoser(false)]
[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
public class Tests
{
private static readonly JsonSerializerOptions s_options = new()
{
Converters = { new JsonStringEnumConverter() },
DictionaryKeyPolicy = JsonNamingPolicy.KebabCaseLower,
};

[Params(BindingFlags.Default, BindingFlags.NonPublic | BindingFlags.Instance)]
public BindingFlags _value;

private byte[] _jsonValue;
private Utf8JsonWriter _writer = new(Stream.Null);

[GlobalSetup]
public void Setup() => _jsonValue = JsonSerializer.SerializeToUtf8Bytes(_value, s_options);

[Benchmark]
public void Serialize()
{
_writer.Reset();
JsonSerializer.Serialize(_writer, _value, s_options);
}

[Benchmark]
public BindingFlags Deserialize() =>
JsonSerializer.Deserialize<BindingFlags>(_jsonValue, s_options);
}

Daha fazla örnek ve bilgi için aşağıdaki linki ziyaret edebilirsiniz.

C# 13

C# 13 ile gelen yenilikler, development deneyimini iyileştirmek ve daha basit kod yazmak yazmaya odaklanıyor.

Params

params anahtar sözcüğü, bir yönteme değişken sayıda argüman aktarmanıza olanak tanır.

Önceki sürümlerde params yalnızca array tipini destekliyordu. C# 13 ile birlikte Span’lar ve IEnumerable<T> uygulayan ve Add yöntemine sahip türler dahil olmak üzere, tanınan herhangi bir koleksiyon türüyle paramları kullanabilirsiniz.

void Check()
{
ParamsMethod(1);
}
void ParamsMethod(params int[] values) // Before C# 13
{
// (1)
}
void ParamsMethod(params IEnumerable<int> values) // After C# 13
{
// (2)
}
void ParamsMethod(params Span<int> values) // After C# 13
{
// (3)
}
void ParamsMethod(params ReadOnlySpan<int> values) // After C# 13
{
// (4) <=
}

Lock Object

C#’taki lock ifadesi, bir kod bloğunun aynı anda yalnızca bir iş parçacığı tarafından yürütülmesini sağlayarak, birden fazla iş parçacığının paylaşılan kaynaklara erişmesini önler. Kod bloğu süresince belirtilen bir nesne üzerinde bir kilit edinerek çalışır ve diğer iş parçacıklarının kilit serbest bırakılıncaya kadar beklemesini sağlar.

public class OldLock
{
private readonly object _lockObj = new();
private static int sharedResource = 0;

public void IncrementResource()
{
lock (_lockObj)
{
sharedResource++;
}
}
}

C# 13 yeni bir Lock türü sunuyor. Daha iyi thread senkronizasyonu için yeni ve verimli bir API’ye sahiptir. Lock.EnterScope() metodu ile, Dispose desenini destekler. Bu, onu diğer tek kullanımlık nesneler gibi using ifadesiyle kullanabileceğiniz anlamına gelir.

public class NewLock
{
private readonly Lock _lockObj = new();
private static int sharedResource = 0;

public void IncrementResource()
{
using (_lockObj.EnterScope())
{
sharedResource++;
}
}
}

Overload Resolution Priority Attribute

Yeni OverloadResolutionPriorityAttribute özelliği, birden fazla aynı isimli metot olduğunda hangi metot çağrısının tercih edileceğini belirlemek için kullanılır. Bu, API geliştirme sırasında önemli bir kolaylık sağlayacaktır.

var service = new MyService();

service.Display("Hello World!");

public class MyService
{
[OverloadResolutionPriority(1)]
public void Display(string chars) =>
Console.WriteLine(chars);

[OverloadResolutionPriority(2)]
public void Display(ReadOnlySpan<char> chars) =>
Console.WriteLine(chars.ToArray());
}

Daha yüksek sayı daha öncelikli demektir. Yukarıdaki örnekte derleyici ReadOnlySpan<char> olan metodu seçer.

System.Text.Json

System.Text.Json, .NET 9 ile birlikte birçok geliştirme alarak JSON işleme sürecini daha güçlü ve esnek hale getirdi.

Customizing Enum Member Names

Yeni JsonStringEnumMemberName niteliği enum değerlerini özelleştirmeyi sağlar.

var json = JsonSerializer.Serialize(MyState.Ready | MyState.InProgress);

Console.WriteLine(json);
// "Ready, In Progress"

[Flags, JsonConverter(typeof(JsonStringEnumConverter))]
enum MyState
{
Ready = 1,
[JsonStringEnumMemberName("In Progress")]
InProgress = 2,
}

Respecting Nullable Notation

.NET 9'da RespectNullableAnnotations seçeneği, JSON serializasyonu ve deserializasyonu sırasında nullable (null atanabilir) referans türlerinin kontrol edilmesine olanak tanır:

JsonSerializerOptions options = new() { RespectNullableAnnotations = true };
string json = """{"Name":null}""";
JsonSerializer.Deserialize<MyPoco>(json, options);
// Hata: 'Name' null olamaz.

Proje ayarlarından da bu özelliği aktif edebilirsiniz.

<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Text.Json.JsonSerializerOptions.RespectNullableAnnotations" Value="true" />
</ItemGroup>

Customizing Identation

.NET 9'da JsonSerializerOptions artık JSON formatlamasını özelleştirmek için daha fazla seçenek sunuyor. İndent karakterini ve boyutunu kontrol edebilirsiniz. Bu özellikle, JSON çıktısının daha okunabilir hale getirilmesi gerektiğinde faydalıdır. Aşağıdaki gibi kullanabilirsiniz:

var person = new
{
Name = "Furkan",
Surname = "Güngör"
};

var options = new JsonSerializerOptions
{
WriteIndented = true,
IndentCharacter = '\t',
IndentSize = 2,
};

string json = JsonSerializer.Serialize(person, options);

Console.WriteLine(json);
// Output:
// {
// "Name": "Furkan",
// "Surname": "Güngör"
// }

JSON Schema Exporter

JsonSchemaExporter ile herhangi bir nesnenizin şemasını export edebilirsiniz.

JsonNode schema = JsonSchemaExporter.GetJsonSchemaAsNode(
JsonSerializerOptions.Default,
typeof(Person));

Console.WriteLine(schema);

public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Title { get; set; }
}
{
"type": [
"object",
"null"
],
"properties": {
"Name": {
"type": [
"string",
"null"
]
},
"Age": {
"type": "integer"
},
"Title": {
"type": [
"string",
"null"
]
}
}
}

Respecting Required Constructor Parameters

JsonSerializerOptions’daki yeni RespectRequiredConstructorParameters seçeneği, gerekli oluşturucu parametreleri eksikse doğrulamayı etkinleştirir.

JsonSerializerOptions options = new()
{
RespectRequiredConstructorParameters = true
};

// Throws exception JsonException
JsonSerializer.Deserialize<RecordDto>("{}", options);

// Throws exception JsonException
JsonSerializer.Deserialize<ClassDto>("{}", options);

record RecordDto(string Value);

class ClassDto(string value)
{
public string Value => value;
}

JsonSerializerOptions.Web

JsonSerializerOptions, JsonSerializerOptions’ın yeni bir singleton örneğine sahiptir — Web.

var me = new Person("Furkan", "Güngör");

string webJson = JsonSerializer.Serialize(
me,
JsonSerializerOptions.Web // Defaults to camelCase naming policy.
);

Console.WriteLine(webJson);
// {"firstName":"Furkan","lastName":"Güngör"}

record Person(string FirstName, string LastName);

𝗡𝗲𝘄 𝗟𝗜𝗡𝗤 𝗠𝗲𝘁𝗵𝗼𝗱𝘀

Count

.NET 9 ile gelen CountBy LINQ metodu, koleksiyonlar üzerinde belirli bir koşula göre elemanların sayısını döndürmek için kullanılır. Bu metod, LINQ'nin standart Count metoduna benzer, ancak CountBy belirli bir koşula dayalı sayma işlemi yapar.

CountBy metodu, IEnumerable<T> koleksiyonları üzerinde kullanılır ve her eleman için verilen koşula uyan öğelerin sayısını döndürür. Bu metod, belirli bir koşulu sağlamak için kullanılan bir lambda alır.

public static int CountBy<T>(this IEnumerable<T> source, Func<T, bool> predicate);
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
public static void Main()
{
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Sayıların 5'ten büyük olanların sayısını almak için CountBy kullanma
int count = numbers.CountBy(n => n > 5);

Console.WriteLine($"5'ten büyük sayıların sayısı: {count}");
}
}

AggregateBy

.NET 9 ile tanıtılan AggregateBy metodu, koleksiyonlar üzerinde bir grup oluşturma işlemi gerçekleştirmenizi sağlar. Bu metod, bir koleksiyondaki elemanları belirli bir özelliğe göre gruplayıp, her grup için birleştirilmiş (aggregate) bir sonuç döndürür. Kısacası, bir koleksiyondaki elemanları gruplar halinde işler ve her grup için bir hesaplama yapar.

AggregateBy, LINQ'deki klasik GroupBy metodunun bir türüdür, ancak AggregateBy, her grup için birleştirilmiş bir işlem yapmanıza olanak tanır. Bu metod, her gruptaki elemanları belirli bir birleştirme fonksiyonu ile işlemeye olanak verir.

public static IEnumerable<TResult> AggregateBy<TSource, TKey, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TKey, IEnumerable<TSource>, TResult> resultSelector);
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
public static void Main()
{
var people = new List<Person>
{
new Person { Name = "Furkan", Age = 28 },
new Person { Name = "Ayşe", Age = 24 },
new Person { Name = "Mehmet", Age = 28 },
new Person { Name = "Zeynep", Age = 24 },
new Person { Name = "Ali", Age = 28 }
};

// Yaşa göre grupla ve her grup için isimlerin birleştirilmiş listesini oluştur
var grouped = people.AggregateBy(
p => p.Age,
(age, group) => new { Age = age, Names = string.Join(", ", group.Select(p => p.Name)) }
);

foreach (var group in grouped)
{
Console.WriteLine($"Yaş: {group.Age} - İsimler: {group.Names}");
}
}
}

class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

Çıktı :

Yaş: 28 - İsimler: Furkan, Mehmet, Ali
Yaş: 24 - İsimler: Ayşe, Zeynep

Index

Index yöntemi ile enumerable bir öğenin indeksini hızlı bir şekilde çıkarmayı sağlar.

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
public static void Main()
{
List<Student> students = [
new("Mahmut"),
new("Ali")];

foreach (var (index, student) in students.Index())
{
Console.WriteLine($"Index : {inde} - Name : {student.Name}");
}
}
}
Index : 0 - Name : Mahmut
Index : 1 - Name : Ali

ASP.Net Core 9

ASP.Net Core ile birlikte gelen birçok yeni özellik var ancak ben dikkatimi çeken ve pratik özellikleri seçtim.

Minimal Api

Yazının başında yer alan ve Microsoft tarafından sağlanan grafikte görebileceğiniz gibi Minimal API’de performans artışı sağlandı.

  • %15 Daha Yüksek RPS (Request Per Second)
  • %93 Daha Az Bellek Kullanımı

Peki bu iyileştirmenin temelleri nereye dayanıyor?

  • Exception Handling %50 daha hızlı. Önceki sürümlerde yapılandırılan exception handler kaldırıldı. Yeni model Native AOT modeline dayanıyor.
  • PGO ve güncellenmeleri
  • System.Text.Json içerisindeki optimizasyonlar
  • LINQ üzerindeki performans iyileştirmeleri

OpenApi

ASP.Net Core 9 ile birlikte uzun zamandır kullandığımız Swashbuckle kütüphanesi default olarak projelerde olmayacak. Bunun yerine Microsoft, Microsoft.AspNetCore.OpenApi isimli yeni geliştirdiği kütüphane ile bu desteği sağlayacak. Aşağıdaki örnek kod ile nasıl konfigürasyon yapabileceğinizi görebilirsiniz.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}

app.Run();

Eğer görselleştirmek için hala swagger kullanmak istiyorsanız aşağıdaki gibi konfigürasyon sağlayabilirsiniz.

app.UseSwaggerUi(c => c.DocumentPath = "/openapi/v1.json");

Keyed Services in Middleware

.Net 8 ile hayatımıza giren KeyedServices özelliği middleware üzerinde kullanılmıyordu. Bu sürüm ile birlikte bu sorun çözüldü.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<MySingleton>("singleton");
builder.Services.AddKeyedScoped<MyScoped>("");

var app = builder.Build();
app.UseMiddleware<MyMiddleware>();
app.Run();

internal class MyMiddleware
{
private readonly RequestDelegate _next;

public MyMiddleware(RequestDelegate next,
[FromKeyedServices("singleton")] MySingleton service)
{
_next = next;
}

public Task Invoke(HttpContext context,
[FromKeyedServices("scoped")]
MyScoped scopedService) => _next(context);
}

public class MySingleton
{ }

public class MyScoped
{ }

Status Code Selector in IExceptionHandler

.Net 8 ile birlikte IExceptionHandler tanıtılmıştı ancak status code olarak 500 kullanıyordu. 500 unhandled exception demektir. Ancak bazen uygulamalarımızda handled exception kullanırız. Genellikle handled exceptionlar için 500 kullanılmaması önerilir. Bu sürüm ile birlikte status code değerini seçmemizi sağlayan bir özellik getirildi.

app.UseExceptionHandler(new ExceptionHandlerOptions
{
StatusCodeSelector = ex => is TimeoutException
? StatusCodes.Status503ServiceUnavailable
: StatusCodes.Status500InternalServerError
});

Hybrid Cache

Caching, hızlı ve ölçeklenebilir uygulamalar oluşturmak için kullandığımız önemli yöntemlerden biridir. İki adet cache seçeneği vardır. In-Memory ve Distributed cache. Her birinin kendine özgü dezavantajları vardır. IMemoryCache hızlıdır ancak tek bir sunucuyla sınırlıdır. IDistributedCache birden fazla sunucuda çalışır.

.NET 9, her iki yaklaşımın en iyilerini birleştiren yeni bir kitaplık olan HybridCache’i sunar. Cache stampede gibi yaygın cache sorunlarını önler. Ayrıca tag based invalidation ve daha iyi performans izleme gibi yararlı özellikler de ekler.

IDistributedCache kullandıysanız, önbelleğe bir değer atamadan önce nesneyi JSON serialization getirmeniz gerekmektedir.Cache read için de aynı işlem geçerlidir. Ayrıca, birçok thread aynı anda önbelleğe ulaştığında, concurrency ile ilgili sorunlar yaşayabilirsiniz.

Yeni HybridCache sizin için gereken tüm işi yapar. Ayrıca, yazılım sistemlerinde birden fazla işlemin, thread veya sunucunun bir kaynağı aşırı yüklemesini veya paylaşılan verilere veya kaynaklara aynı anda erişirken gereksiz işler yapmasını önlemek için kullanılan teknikleri ifade eden “Stampede koruması”nı da uygular.

Hybrid Cache ile yapabilecekleriniz;

  • L1 : Memory Cache
  • L2 : Distributed Cache (Redis etc.)
  • Cache stampede
  • Tag-based cache invalidation
  • Configurable serialization

Hybrid Cache kullanmak için projenize Microsoft.Extensions.Caching.Hybrid eklemeniz gerekir.

builder.Services.AddHybridCache(); // Not shown: optional configuration API.
builder.Services.AddHybridCache(options =>
{
// Maximum size of cached items
options.MaximumPayloadBytes = 1024 * 1024 * 10; // 10MB
options.MaximumKeyLength = 512;

// Default timeouts
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(30),
LocalCacheExpiration = TimeSpan.FromMinutes(30)
};
});

Artık HybridCache özelliğini kullanabilirsiniz.

public class SomeService(HybridCache cache)
{
public async Task<SomeInformation> GetSomeInformationAsync
(string name, int id, CancellationToken token = default)
{
return await cache.GetOrCreateAsync(
$"someinfo:{name}:{id}", // Unique key for this combination.
async cancel => await SomeExpensiveOperationAsync(name, id, cancel),
token: token
);
}
}

GetOrCreate metodu ile ilgili unique key varsa direkt okuyacak ancak yoksa aldığı Action parametresi ile ilgili kodu çalıştıracaktır.

Eğer distributed cache kullanıyorsanız Hybrid Cache otomatik olarak kullandığınız teknolojiyi yakalar.

// Add Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "your-redis-connection-string";
});

builder.Services.AddHybridCache();

Kullandığımız teknojiler zaman içerisine güncellenir ve yeni özelliklere sahip olurlar. Bu tarz framework update işlemlerinde kodunuzun en az şekilde etkilenmesini istiyorsanız projelerinizi güncel sürümde tutmalısınız. Yeni bir sürüme geçmenin en kolay yolu bir önceki sürümde hazır bir şekilde beklemektir.

Eğer .Net 9'a geçmek istiyorsanız breaking changes dökümanlarınızı muhakkak okumalı ve kendi projeniz ile karşılaştırmalısınız. Aşağıya örnek olması için EF Core 9 ve .Net 9 için döküman linkleri bırakıyorum.

Sign up to discover human stories that deepen your understanding of the world.

--

--

Furkan Güngör
Furkan Güngör

Written by Furkan Güngör

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

Responses (2)

Write a response