Yazılım

.NET 7 ile Gelen Bazı Harika Yenilikler

Bildiğimiz gibi .NET Conf 2022, 8-10 kasım arasında gerçekleşti. Konferans sırasında ise .NET 7 ve C# 11 çevresinde gelen heyecan verici yeniliklerden ve performans iyileştirmelerinden bahsedildi. Bu release de özellikle biz developer’ların daha “hızlı”, “lightweight” ve daha kolay “cloud-native” application’lar geliştirebilmesi gibi harika konulara odaklanılmış.

Bu makale kapsamında ise beğendiğim bazı yeniliklere değinmeye çalışacağım.

NOT: Öncelikle .NET 7 release’ine henüz sahip değilseniz, buradan indirebilirsiniz.

Her .NET release’inde olduğu gibi bu release içerisinde de major denilebilecek bir seviyede, hatta en iyi seviyede performance iyileştirmesi yapıldığı söylendi. Gerçekten de performans konusunda güzel bir iş çıkartılmış. Bu iyileştirmelerin önemli bir kısmı ise JIT tarafında gerçekleştirilmiş ve bu konuya kısaca değinmek istiyorum.

Bildiğimiz gibi JIT, MSIL code’u runtime’da native code’a çevirmekten ve yönetmekten sorumludur. Application’larımızın performanslı çalışabilmesi için arka planda bulunduğu environment’ı/process’i de dikkate alarak, bir çok farklı optimizasyonlar gerçekleştirmektedir. Tahmin edebileceğimiz gibi bu tarz just-in-time performans optimizasyonları ise doğası ve bazı tradeoff’lar gereği biraz zaman alan işlemlerdir. Örneğin JIT tarafından optimizasyonlar yapılmadığında veya tam yapılmadığında application’ın start-up time’ı hızlanabilir fakat fonksiyonalitesi yani throughput’u düşebilir. Bu tarz tradeoff’ları azaltabilmek için JIT, bildiğimiz gibi .NET Core 3 ‘den bu yana Tiered Compilation ‘ı default olarak kullanmaya başlamıştır. Böylece JIT daha iyi bir performans optimizasyonu gerçekleştirebilmek için ilgili method’ları sadece bir kere compile etmek yerine, kullanım istatistiğine göre birden fazla recompile ederek runtime’da hot-swap işlemini gerçekleştirebilmektedir.

.NET 7 ile birlikte JIT tarafında yapılan performans iyileştirmelerinde On-stack replacement tekniğinden de yararlanılarak, JIT ‘in azaltmaya çalışmış olduğu bu tradeoff’lar tamamen handle edilmeye çalışılmış. Böylece JIT ‘in gerçekleştirmiş olduğu optimizasyonları sadece method invocation’ları arasında değil, ilgili method çalışırken dahi gerçekleştirebilmesi sağlanmış.

Bunların dışında Threading, Networking, Collections, LINQ vb. gibi bir çok önemli noktada da harika performans çalışmaları yapılmış. Kısacası .NET 7 sürümüne geçiş ile, default olarak güzel bir performans kazanımı elde edebileceğiz.

Console Application’lar için Native AOT

Öncelikle beni heyecanlandıran Native Ahead-of-time (AOT) konusu ile başlamak istiyorum.  Bildiğimiz gibi .NET takımı bir süredir Native AOT ile ilgili bazı çalışmalar yapmaktaydı ve .NET 7 ile birlikte bu çalışmaları experimental statüs’ünden çıkartarak mainline development’a dahil edeceklerini duyurmuşlardı. Bu release ile birlikte artık console application’ları ve class library’leri için Native AOT official olarak bizlerle.

Native AOT kısaca ilgili code’u run-time yerine compile-time‘da “native” olarak oluşturmaktadır. Kısacası application’ı publish ederken belirtilen runtime’a göre ilgili IL code’unu native code’a compile etmektedir. Böylece Native AOT application’ları çalışırken JIT‘e ihtiyaç duymamaktadır. Bir başka değişle, .NET runtime’ı olmayan environment’larda da Native AOT application’larımızı çalıştırabilmekteyiz. Tabi her ne kadar bu durum daha önce farklı özellikler altında da bizlere sunulsa da, örneğin “Ready to Run” gibi, Native AOT ile birlikte bu concept daha iyi bir noktaya getirilmiş.

Faydaları ise kısaca;

  • JIT gereksinimini kaldırmakta diyebiliriz. (Tabi bu konu biraz tartışmaya açık söz konusu start-up time’ı yerine runtime performansı olunca. Bildiğimiz gibi JIT compiler bulunduğu ortamı analiz ederek, kodumuz için en iyi optimizasyon işlemini de sağlıyor.)
  • Application start up time’ını oldukça hızlandırmakta.
  • Daha az memory kullanımı sağlamakta.
  • Compile olduğunda ise application’ın disk size’ını “self-contained” publish’e kıyasla oldukça azaltmaktadır.
  • Farklı programlama dilleri tarafından kullanılabilecek native library’ler geliştirilebilmesini de sağlamaktadır.

Native AOT ‘nin heyecan verici olmasının yanı sıra, maalesef bazı limitation’ları da bulunmakta.

  • Eğer runtime code generation’ına ihtiyacınız varsa (System.Reflection.Emit), maalesef Native AOT ile kullanılamayacak.
  • Dynamic olarak loading’de gerçekleştirilememekte (Assembly.LoadFile).
  • Şuanlık console application’ları ve class library’leri için kullanabilmekteyiz.
  • Şuanlık tüm runtime library’leri henüz full olarak Native AOT için uygun değil.
  • Trimming’e de ihtiyaç duymaktadır ve trimming’in de kendisine has limitation’ları bulunmaktadır. Örneğin yine dynamic assembly loading ve execution, reflection-based serializers vb. Bu limitation detaylarına ise, buradan ulaşabilirsiniz.

Her ne kadar şimdilik limitation’ları bulunsa da, ilerleyen dönemlerde güzel bir noktaya geleceğinden eminim.

Hızlıca bir test gerçekleştirebilmek için target framework’ü .NET 7 olan bir console application oluşturalım. Ardından verilen input’un palindrome olup olmadığını kontrol eden basit kod parçasını Program.cs dosyası içerisine alalım.

string? input = Console.ReadLine();

bool result = IsPalindrome(input);

Console.WriteLine($"The input '{input}' is a palindrome: {result}");
Console.ReadLine();

static bool IsPalindrome(string? input)
{
    if (string.IsNullOrEmpty(input))
    {
        return false;
    }

    bool result = true;

    for (int i = 0; i < input.Length; i++)
    {
        if (input[i] != input[(input.Length - 1) - i])
        {
            result = false;
        }
    }

    return result;
}

Şimdi bu application’ı native olarak compile edebilmek için, application’ın project file’ına aşağıdaki gibi bir property eklememiz gerekmektedir.

<PublishAot>true</PublishAot>

Eğer property eklemek istemiyorsak da, publish command’ı içerisinde de parametre olarak geçebiliriz.

-p:PublishAot=true

Ardından application’ı istediğimiz bir runtime identifier’ı belirterek, native bir şekilde compile edebileceğiz. Örneğin Windows environment’ı için “win-x64”, Linux için ise “linux-arm64” identifier’larını kullanabiliriz.

NOT: Eğer application’ı Ubuntu 20.04 üzerinde compile edersek, sadece bu versiyonda veya üst versiyonlarında çalışmaktadır. Kısacası compile etmek için kullanmış olduğumuz Linux versiyonuna da dikkat etmemiz gerekmektedir.

Şimdi test edebilmek için aşağıdaki gibi bir Dockerfile kullanalım.

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build

# Install NativeAOT build prerequisites
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       clang zlib1g-dev

WORKDIR /source

COPY . .
RUN dotnet publish -c release -r linux-x64 -o /app

FROM debian:bullseye-slim
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["/app/NativeAOTTest"]

Bu noktada dikkat edersek compile edebilmek için Debian-based “dotnet/sdk:7.0″image’ini kullanırken, runtime olarak ise içerisinde .NET runtime barındırmayan “debian:bullseye-slim” image’ini kullanacağız.

Ayrıca application’ı Linux machine üzerinden publish etmeden önce, aşağıdaki ilgili paket’e de sahip olmamız gerekmektedir.

  • Ubuntu (18.04+): “sudo apt-get install clang zlib1g-dev”
  • Alpine (3.15+): “sudo apk add clang build-base zlib-dev”

Container’ı yukarıda gördüğümüz gibi build edip çalıştırdığımız da, application’ın sorunsuz bir şekilde çalıştığını görebiliriz.

Native AOT‘nin özellikle serverless solution’lar için oldukça kullanışlı olabileceğini düşünmekteyim. Özellikle execution duration’larının ve start-up (cold-start) zamanlarının önemli olduğunu göz önüne alırsak, bu noktada güzel bir kazanç sağlayabiliriz.

Built-in Container Support

Bu release kapsamında bizlerin daha efektif ve hızlı cloud-native application’lar geliştirebilmemiz üzerine odaklanıldığını söylemiştim. İşte tam da bu kapsamda “Built-in Container Support” desteği de bunlardan birisi.

Çok küçük bir özellik gibi görünse de, hızlı bir şeyler yapabilmek için ben oldukça kullanışlı buldum. Özellikle custome-made bir Dockerfile ‘a ihtiyacınız yoksa, application’ımızı publish ederken belirteceğimiz bir parametre ile containerization işlemini gerçekleştirebileceğiz.

Bu işlem için sadece aşağıdaki package’ı NuGet üzerinden eklememiz gerekmektedir.

dotnet add package Microsoft.NET.Build.Containers

Ardından publish işlemini aşağıdaki gibi gerçekleştirdiğimizde, container image’i de otomatik olarak oluşturulmuş olacak.

dotnet publish --os linux --arch x64 -c Release -p:PublishProfile=DefaultContainer

Ben aşağıdaki gibi bir .NET 7 Web API projesi için bir deneme gerçekleştirdim.

dotnet new webapi -n my-test-api

Görebileceğimiz üzere container başarıyla oluşturuldu.

Base image olarak ise Debian-based Linux image’leri default olarak kullanılmaktadır. Eğer farklı bir distribution kullanmak istiyorsak da, bu işlemi aşağıdaki gibi “ContainerBaseImage” property’si ile gerçekleştirebiliriz.

<ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:7.0-alpine</ContainerBaseImage>

Container ismi olarak ise default “AssemblyName”, tag olarak ise “Version” property’si kullanılmaktadır.

Dilersek bunları da aşağıdaki gibi ilgili property’ler ile değiştirebilmekteyiz.

<ContainerImageName>my-app</ContainerImageName>
<Version>1.2.3-alpha2</Version>

Limitation olarak ise şimdilik sadece Linux-based container’ları desteklemektedir.

MemoryCacheStatistics

Bildiğimiz gibi ASP.NET Core içerisinde in-memory caching için kullanabileceğimiz en kolay yöntem, IMemoryCache ‘i kullanmak.

Uzun bir zamandır bizlerle olan IMemoryCache ‘e, .NET 7 ile birlikte metric desteği için yeni bir API eklendi. Artık MemoryCacheStatistics ile birlikte cache’in nasıl kullanıldığı bilgisinin yanı sıra, cache’in tahmini boyut bilgisine de erişebileceğiz.

Application’ın in-memory cache ile ilgili metric bilgilerine ulaşabilmek ve ona göre aksiyonlar alabilmek, application’ın sağlığı açısından faydamıza olacaktır.

Bu metric bilgilerine erişebilmek için ise IMemoryCache üzerinden “GetCurrentStatistics()” method’unu çağırmamız gerekmektedir. Ayrıca bu metric’leri takip edebilmek için ise ister EventCounters API‘ından yararlanarak dotnet-counters tool’u aracılığı ile bu bilgilere erişebiliriz veya .NET metrics API‘ı ve OpenTelemetry ‘den yararlanabiliriz.

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly IMemoryCache _memoryCache;

    public WeatherForecastController(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    [HttpGet("stats")]
    public ActionResult<MemoryCacheStatistics> GetStats()
    {
        return Ok(_memoryCache.GetCurrentStatistics());
    }
}

Bu işlemin öncesinde ise IMemoryCache‘i service collection’a eklerken, “TrackStatistics” parametresini “true” olarak set etmemiz gerekmektedir.

builder.Services.AddMemoryCache(c => c.TrackStatistics = true);

Central NuGet Package Management

Çok büyük bir feature olmasa da birden çok proje tarafından kullanılan ortak NuGet package’larının versiyonlarını, merkezi bir lokasyondan yönetebilmek hoşuma gitti.

Bunun için ilgili solution’ın root klasöründe Directory.Packages.props adında bir dosya oluşturmamız ve içerisinde istediğimiz package’ları aşağıdaki gibi tanımlamamız gerekmektedir.

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>

  <ItemGroup>
    <PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
  </ItemGroup>
</Project>

Ardından istediğimiz proje içerisinde sadece ilgili package’ın ismini reference olarak eklememiz yeterli olacaktır.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" />
  </ItemGroup>
</Project>

Required Members

C# 11 tarafında ise gelen “required” keyword’ü ile, parameter null checking özelliği oldukça kullanışlı bir hale gelmiş.

public class MyClass
{
    public required string MyRequiredParam { get; init; }
    public string? MyOptionalParam { get; init; }
}

Yukarıda gördüğümüz gibi “required” keyword’ünü kullandığımızda, ilgili class’ı initialize ederken “MyRequiredParam” parametresini set etmemiz zorunlu bir hale gelecektir.

Microsoft Orleans 7.0

Actor-model’e her zaman ayrı bir ilgi duymuşumdur ve bu konuda Orleans Project‘i de yakından takip etmekteyim. Daha önce Orleans ile ilgili bir kaç farklı makale ve bir adet sunum gerçekleştirmiştim.

İlgili içeriğe buradan erişebilirsiniz.

.NET 7 kapsamında Orleans tarafında da harika performans iyileştirmeleri gerçekleştirilmiş. Immutability tarafında iyileştirmeler ve yeni bir serialization getirilmiş. Ben bir kaç yıl önce Orleans üzerinde çalışırken ozamanlar zaten harika ve performanslı bir tool idi. Üzerinde bir kaç farklı application geliştirmiştim. Şimdiki halini ve performansını açıkçası merak ediyorum.

EF7 JSON Columns & Bulk Operations

Bildiğimiz gibi SQL Server ‘ın JSON columns desteği uzun bir süredir bizlerle. EF tarafında da JSON columns desteği artık bu release ile getirildi. Artık LINQ ile birlikte SQL Server tarafında JSON aggregate’lerine yönelik query’ler ve farklı operation’lar gerçekleştirebileceğiz.

Örneğin aşağıdaki gibi bir schema’ya sahip olduğumuzu düşünelim.

public class Product
{
    public string Name { get; set; }
    public string Description { get; set; }
    public Price PriceDetails { get; set; }
}

public class Price
{
    public decimal List { get; set; }
    public decimal Retail { get; set; }
}

Bu noktada PriceDetails bilgisini JSON column olarak tutmak istiyoruz. Bunun için yapmamız gereken tek şey, aşağıdaki gibi model configuration’ı sırasında “ToJson()” method’unu call etmek.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>().OwnsOne(
        product => product.Price, navigation =>
        {
            navigation.ToJson();
        });
}

Gerisi ise LINQ gücümüze kalmış.

Bulk operation’lar tarafında ise “ExecuteUpdateAsync” ve “ExecuteDeleteAsync” olmak üzere iki yeni method getirilmiş. Bu method’ları kullanarak istediğimiz LINQ ile bulk işlemler gerçekleştirebileceğiz.

await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn < priorToDateTime)).ExecuteDeleteAsync(); await context.Tags .Where(t => t.Posts.All(e => e.PublishedOn < priorToDateTime)) .ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));

Bu tarz bulk işlemleri gerçekleştirebilmek için farklı EF extension’ları kullanmak yerine, EF içerisine getirilmiş olmaları gayet tatlı olmuş.

Rate-Limiting Middleware

Son olarak da ASP.NET Core tarafında getirilen “Rate-Limiting” middleware’inden bahsetmek istiyorum.

Bildiğimiz gibi geliştirmiş olduğumuz API ‘ların rate-limiting’e sahip olması aslında önemli bir konu. Çünkü hem geliştirmiş olduğumuz API ‘ın overwhelm olup performansının azalmamasını hem de DoS gibi saldırılara karşı bir çeşit güvenlik mekanizmasına sahip olabilmemizi sağlamaktadır. Elbette özellikle public API ‘lar geliştiriyorsak.

Kullanımı oldukça basit bir middleware ve “Fixed window”, “Sliding window”, “Token bucket” ve “Concurrency” olmak üzere 4 farklı rate-limiting policy’si ile gelmektedir. İstediğimiz endpoint seviyesinde de attach edebiliyoruz.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(rateLimitingoptions =>
    rateLimitingoptions.AddFixedWindowLimiter(policyName: "fixed", options =>
    {
        options.PermitLimit = 100;
        options.Window = TimeSpan.FromSeconds(10);
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = 2;
    }));

Örneğin yukarıdaki gibi bir “Fixed window” policy, “10” saniyelik window’lar içerisinde maksimum “100” request’e izin vermektedir.

Daha sonra ise istediğimiz seviyede aktif veya pasif bir hale getirebilmekteyiz.

app.MapControllers().RequireRateLimiting("fixed");
[ApiController]
[Route("[controller]")]
[EnableRateLimiting("fixed")]
public class WeatherForecastController : ControllerBase
{
    private readonly IMemoryCache _memoryCache;

    public WeatherForecastController(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    [HttpGet("stats")]
    [DisableRateLimiting]
    public ActionResult<MemoryCacheStatistics> GetStats()
    {
        return Ok(_memoryCache.GetCurrentStatistics());
    }
}

Toparlayalım

Bu makale kapsamında ilk bakışta hoşuma giden bazı yeniliklerden bahsetmeye çalıştım. Bu release de beni şaşırtmadı çünkü her bir release içerisinde gerçekten harika yenilikler ve performans iyileştirmeleri gerçekleştiriliyor. Harika bir iş kısacası!

Özellikle CLR tarafında yapılan iyileştirmeler, gerçekten harika bir iş. Ayrıca Native AOT konusunun da nerelere gideceğini oldukça merak etmekteyim. Tabi bunların yanında burada değinmediğim bir çok farklı yeni özellikler eklendi ve iyileştirmeler de gerçekleştirildi. Örneğin loop ve reflection performans optimizasyonları, yeni eklenen Archive Tar API‘ı gibi bir çok farklı yenilikler ve iyileştirmeler de mevcut.

Eğer sizlerinde beğendiği farklı noktalar varsa, yorumlarınızı bekliyorum.

Referanslar

https://devblogs.microsoft.com/dotnet/announcing-dotnet-7
https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7
https://devblogs.microsoft.com/dotnet/announcing-builtin-container-support-for-the-dotnet-sdk
https://devblogs.microsoft.com/dotnet/whats-new-in-orleans-7
https://learn.microsoft.com/en-gb/aspnet/core/performance/rate-limit?view=aspnetcore-7.0

İlgili Makaleler

2 Yorum

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Başa dön tuşu