Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

.NET Aspire Nedir? Nasıl Kullanılır? Detaylıca İnceleyelim…

Merhaba,

Bu içeriğimizde, bulut-yerel(cloud-native) uygulamaları oldukça efektif ve hızlı bir şekilde geliştirebilmek ve servisler arasındaki entegrasyonları docker teknolojisine dayalı bir şekilde daha hızlı çözüme kavuşturabilmek için tasarlanmış bir araç seti olan .NET Aspire‘ı inceliyor olacağız. Bu araç seti ile uygulamalarımız daha gözlemlenebilir(observable), üretime(production) hazır ve yönetilebilir hale getirilebilmektedir ve bunun için çeşitli araçlar, şablonlar ve NuGet paketleri sunulmaktadır. Şimdi gelin fazla uzatmaksızın .NET Aspire’ın ne olduğunu ve hangi mimarilere, ne gibi niteliklerle katkıda bulunduğunu değerlendirmeye başlayalım…

.NET Aspire Nedir?

.NET Aspire, cloud-native uygulamalar geliştirme süreçlerinde deneyimi iyileştirmek ve bu süreçlerdeki belirli endişeleri ele almak için tasarlanmış bir araç setidir. Arkaplanda Docker kullanarak dinamik bir şekilde .NET uygulamalarımızın, içerisinde uygulama için gerekli olan tüm bağımlılıkları, kütüphaneleri ve yapılandırmaları barındıran docker image’larını oluşturmakta ve bu oluşturulan image’ları varsayılan olarak Azure Container Registry(ACR)‘e depolayabilsede, istendiği taktirde Google Container Registry(GCP)‘e, Amazon Elastic Container Registry(ECR)‘e veyahut Docker Hub‘a da depolayabilmektedir.

Özellikle cloud-native uygulama deyince akıllara direkt microservice olarak yapılandırılmış bir mimari gelebilir. Haliyle bu zandan yola çıkarak .NET Aspire’ın en büyük getirisi olarak, özellikle birden fazla proje ve dependency’nin birlikte çalışma yeteneğini daha da güçlendirmesi olduğunu söyleyebiliriz. Tabi biryandan da konteyner mimarisini arka planda otomatik yönetmesinden ve bundan dolayı deploy süreçlerini basitleştirmesinden dolayı cloud-native uygulamaların geliştirme süreçlerinin bundan böyle muhtemel vazgeçilmezi olacağı kanaatindeyim.

.NET Aspire’ın Başlıca Özellikleri Nelerdir?

.NET Aspire, distributed uygulamalar oluşturulmasına ve çalıştırılmasına yardımcı olan bir dizi araç ve model sağlamaktadır. Özellikle aşağıdaki satırlarda detayları verildiği üzere orkestrasyon, entegrasyon ve araçsal zenginlik açısından sürece ciddi nitelikler kazandırmaktadır;

  • Orkestrasyon(Orchestration)
    .NET Aspire, yerel geliştirme ortamları(local development environments) için çoklu proje uygulamalarını ve bu uygulamaların bağımlılıklarını(dependencies) çalıştırmak ve birbirleriyle bağlantı kurmalarını sağlamak için özellikler sağlamaktadır. Bu özelliklerden en önemlisi diyebileceğimiz service discovery’dir. Evet, servisler arası keşif süreci .NET Aspire sayesinde oldukça basit ve dinamik hale getirilebilmektedir.
  • Entegrasyonlar(Integrations)
    .NET Aspire; Redis, PostgresSQL, RabbitMQ, Keycloak vs. gibi yaygın olarak kullanılan servisler için gerekli olan NuGet paketlerini barındırmakta ve uygulamanın tutarlılığını sorunsuz bir şekilde sağlayabilmek için de standartlaştırılmış arayüzlerle entegrasyonları gerçekleştirmektedir.
  • Araçlar ve Şablonlar(Tooling)
    .NET Aspire; Visual Studio, Visual Studio Code ve .NET CLI için projelerin hızlı ve kolay bir şekilde oluşturulmasına yardımcı olacak şablonlar ve araçlar içermektedir. Yani, önceden tasarlanmış microservice mimarisine sahip bir çalışmanın hızlıca .NET Aspire ortamında tasarlanması ve gerekli yapılandırmaların standartlaştırılması kullanılan IDE’nin araç ve şablon desteğine bağlı olarak hızlıca gerçekleştirilebilmekte ve kalındığı yerden .NET Aspire desteğiyle yola devam edilebilmektedir.

Uzun lafın kısası, .NET Aspire, özellikle cloud-native uygulama geliştirme süreçlerini hızlandırmak ve kolaylaştırmak amacıyla tasarlanmış bir çözüm paketidir diyebiliriz.

.NET Aspire’ın Microservice Mimarilerindeki Getirileri Nelerdir?

.NET Aspire, microservice mimarilerinde geliştirme ve yönetim sürecini büyük ölçüde basitleştirmekte ve hızlandırmaktadır. Özellikle birden fazla mikro servisin yönetilmesi ve çalıştırılması için yardımcı araçlar sunmaktadır. Ki bu araçlar servislerin birbirleriyle nasıl iletişim kuracağını ve birlikte nasıl çalışacağını test etmenizi kolaylaştırmaktadır. Tabi her mikro servisin performansını ve güvenilirliğini izlemek oldukça kritik arz edeceğinden dolayı .NET Aspire bu açıdan da büyük avantajlar sağlamaktadır. Ve ayrıca geliştirilen uygulamanın Visual Studio ve .NET CLI gibi araçlarla hızlı prototiplenmesini ve deploy edilmesini sağlaması da zamansal açıdan ciddi maliyetleri törpülemektedir. İşte bu ve bunlara benzer nedenlerden dolayı microservice mimarileri açısından .NET Aspire oldukça kıymetli bir gelişmedir diyebiliriz.

.NET Aspire Orchestration Süreci

Biliyorsunuz ki, normal şartlarda local ortamda uygulamaları yapılandırmak ve servisleri ayağa kaldırabilmek için genellikle Docker Compose gibi bir nimetten istifade edilmektedir. Ancak bazen basit bir kurulum için bile environment variable’lardan tutun connection string’lere kadar birçok metinsel içeriğin yapılandırılması gerekebilmektedir. Keza docker’a aşinalık söz konusu değilse bu durumlar oldukça zorlayıcı ve uğraştırıcı olabilmektedir.

.NET Aspire Orchestration ise uygulamanın yapılandırmasını local geliştirme sürecine odaklı bir şekilde basitleştirmektedir.

.NET Aspire Orchestration, production ortamlarında kullanılan Kubernetes gibi daha kapsamlı sistemlerin yerini almayı AMAÇLAMAMAKTADIR!

Genellikle service discovery, environment variable ve container yapılandırmalarını kolaylaştırmakta ve bir yandan da uygulama ayrıntılarıyla uğraşma ihtiyacını ortadan kaldıracak bir dizi soyutlama sağlamaktadır.

Şimdi bunu temelden tecrübe ederek incelemeye ve böylece yavaş yavaş .NET Aspire’ın derinliklerine dalmaya geçebiliriz. Tabi bunun için öncelikle .NET Aspire desteğinin mevcut bir proje yapısında nasıl şekillendirileceğini ele alarak başlayacağız. Ardından sıfırdan başlayan projelerde nasıl bir yaklaşım sergilenebileceğini değerlendireceğiz. Ee haliyle bu süreçte bir yandan da .NET Aspire’ın proje yapısını anlıyor olacak, belli başlı metotların neler olduğuna ve .NET Aspire’ın iş mantığındaki akışın nasıl cereyan ettiğine dair ciddi aydınlanmalar yaşayacağız. Hadi buyurun başlayalım…

1. Mevcut Bir Mimaride .NET Aspire Desteğini Sağlamak

.NET Aspire Nedir? Nasıl Kullanılır? Detaylıca İnceleyelim...Şimdi bir senaryo üzerinden .NET Aspire’ın kullanımını örneklendirecek, ardından incelemeye devam edeceğiz. Senaryomuz şöyle olacaktır; ServiceA ve ServiceB isminde iki adet Asp.NET Core uygulaması oluşturacağız. Bu uygulamalar aralarında aşağıdaki gibi iletişim modellerine sahip olacaktır;

  • ServiceA, ServiceB‘ye http request’te bulunacak ve cevap alacaktır.
  • ServiceB ise ServiceA‘ya message broker(RabbitMQ) üzerinden bir mesaj yollayacaktır.

Bu uygulamaların farazi olarak aşağıdaki gibi geliştirildiklerini varsayabiliriz;

‘ServiceA’;

using MassTransit;
using ServiceA.Consumers;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient();
builder.Services.AddMassTransit(configurator =>
{
    configurator.AddConsumer<ServiceBSentMessageConsumer>();
    configurator.UsingRabbitMq((context, _configure) =>
    {
        _configure.Host(builder.Configuration.GetConnectionString("RabbitMQ"));
        _configure.ReceiveEndpoint("servicea-message-queue", e => e.ConfigureConsumer<ServiceBSentMessageConsumer>(context));
    });
});

var app = builder.Build();

app.MapGet("/", async (HttpClient httpClient) =>
{
    var response = await httpClient.GetAsync("https://localhost:7257/api/data");
    response.EnsureSuccessStatusCode();
    var data = await response.Content.ReadAsStringAsync();
    return Results.Ok(data);
});

app.Run();

‘ServiceB’;

using MassTransit;
using Shared;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMassTransit(configurator =>
{
    configurator.UsingRabbitMq((context, _configure) =>
    {
        _configure.Host(builder.Configuration.GetConnectionString("RabbitMQ"));
    });
});

var app = builder.Build();

app.MapGet("/send-message", async (ISendEndpointProvider sendEndpointProvider) =>
{
    var sendEndpoint = await sendEndpointProvider.GetSendEndpoint(new Uri($"queue:servicea-message-queue"));
    await sendEndpoint.Send(new Message { Text = $"Message sent : {DateTime.Now}" });
});

app.MapGet("/api/data", () => "Hello World! : ServiceB");

app.Run();

Bu şekilde hazır/mevcut bir mimaride .NET Aspire desteğini sağlayabilmek için yapılması gereken sistemdeki tüm projeleri tek tek ekleyebileceğimiz bir .NET Aspire yapısı oluşturmaktır. Bunun için servislerden birine sağ tıklayarak aşağıdaki görselde olduğu gibi ‘Add’ -> ‘.NET Aspire Orchestrator Support’ kombinasyonu eşliğinde bu servis ile birlikte .NET Aspire yapısını genel solution’a ekleyebilir ve sonrasında da diğer servisi de yine aynı metotla bu oluşturulan .NET Aspire yapısına dahil edebiliriz..NET Aspire Nedir Nasıl Kullanılır Detaylıca İnceleyelim...Bu kombinasyon ile ilk servis eşliğinde .NET Aspire’ı oluşturmaya çalıştığımızda aşağıdaki görselde olduğu gibi oluşturulacak .NET Aspire ile ilgili proje yapılanmalarından yani bir başka deyişle şablonlarından bahsedildiğini göreceğiz..NET Aspire Nedir Nasıl Kullanılır Detaylıca İnceleyelim...Dizin ve isimlendirme yapılandırmasına istenilen dokunuşlarda bulunulduktan sonra Ok diyerek .NET Aspire yapısını inşa ettirebiliriz..NET Aspire Nedir Nasıl Kullanılır Detaylıca İnceleyelim... Bu işlem neticesinde solution’a yandaki görselde olduğu gibi ‘NetAspire.Example.AppHost’ ve ‘NetAspire.Example.ServiceDefaults’ isminde iki adet proje eklenecektir. Yani bu işlem neticesinde ilgili solution’a .NET Aspire desteği dahil edilmiştir diyebiliriz.

Bu projelerden özellikle ‘NetAspire.Example.AppHost’, sistemdeki tüm projelerin .NET Aspire sayesinde orkestratör edilmesini sağlayacak olan ana projedir. Yani sistemde mevcut olup, .NET Aspire desteği beklenen tüm servisler bu uygulamanın ‘Program.cs’ dosyasında aşağıdaki gibi yönetilebilmek için referans edilmektedirler.

var builder = DistributedApplication.CreateBuilder(args);

builder.AddProject<Projects.ServiceA>("servicea");

builder.AddProject<Projects.ServiceB>("serviceb");

builder.Build().Run();

.NET Aspire Nedir Nasıl Kullanılır Detaylıca İnceleyelim...Tabi referans edilebilmek için bu projeye sistemde .NET Aspire ile kullanılacak tüm projeler referans edilmelidir. Bunu tabi ki de manuel yapmıyoruz. Yukarıdaki satırlarda bahsedilen ‘Add’ -> ‘.NET Aspire Orchestrator Support’ işlevsel kombinasyonu hangi servis için gerçekleştirirsek o servis otomatik olarak bu proje tarafından fiziksel referans edilecek ve biryandan da ‘Program.cs’inde de AddProject metodu ile referans gerçekleştirilecektir.

‘NetAspire.Example.ServiceDefaults’ projesi ise service discovery, telemetry ve health checks gibi yeniden kullanılabilen yapılandırmaları ve bağımlılıkları yönetmek için gerekli olan extension metotları vs. barındırmaktadır. Haliyle kullanılabilir olması için sistemdeki tüm servisler/projeler tarafından referans edilmektedir ki bu işlemi de yine .NET Aspire desteği otomatik sağlayacaktır.

Burada dikkat edilmesi gereken husus şudur ki; bizler solution içerisindeki bir projeyi .NET Aspire desteğine eklediğimiz vakit o projenin ‘Program.cs’ dosyasında aşağıdaki değişiklikler meydana gelmektedir:

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();
.
.
.
var app = builder.Build();
.
.
.
app.MapDefaultEndpoints();
.
.
.
app.Run();

Burada AddServiceDefaults servisiyle MapDefaultEndpoints middleware’i göze çarpmaktadır.

  • AddServiceDefaults; ServiceDefaults projesinden gelen bir extension metottur. Yukarıdaki satırlarda bahsedildiği gibi telemetry, health checks, service discovery vs. gibi yapılandırmaları ve gerekli bağımlılıkları ilgili proje için konfigüre etmektedir. Aşağıdaki gibi bir içeriğe sahiptir:
        public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
        {
            builder.ConfigureOpenTelemetry();
    
            builder.AddDefaultHealthChecks();
    
            builder.Services.AddServiceDiscovery();
    
            builder.Services.ConfigureHttpClientDefaults(http =>
            {
                // Turn on resilience by default
                http.AddStandardResilienceHandler();
    
                // Turn on service discovery by default
                http.AddServiceDiscovery();
            });
    
            // Uncomment the following to restrict the allowed schemes for service discovery.
            // builder.Services.Configure<ServiceDiscoveryOptions>(options =>
            // {
            //     options.AllowedSchemes = ["https"];
            // });
    
            return builder;
        }
    

    .NET Aspire’ın getirisi olan ServiceDefaults projesi, içerisinde gelen Extensions.cs dosyasını ve işlevselliğini paylaşmak için özel olarak tasarlanmış bir yapıya sahiptir. İlk bakışta, yapılandırma faaliyetleri ve zengin işlevsel niteliklerinden dolayı farklı projelerde kullanılabilir hissi doğuruyor olsa da böyle bir ihtiyaca karşın bu davranıştan kaçınılması gerektiğini özellikle vurgulamak ve bu tarz amaçlar için geleneksel yöntemlerin benimsenmesi gerektiğinin tavsiye edildiğini ifade etmek isterim.

  • MapDefaultEndpoints; health checks endpoint’lerini (/health – /alive) sisteme dahil eder. İçerik olarak aşağıdaki gibi bir yapıya sahiptir:
        public static WebApplication MapDefaultEndpoints(this WebApplication app)
        {
            // Adding health checks endpoints to applications in non-development environments has security implications.
            // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
            if (app.Environment.IsDevelopment())
            {
                // All health checks must pass for app to be considered ready to accept traffic after starting
                app.MapHealthChecks("/health");
    
                // Only health checks tagged with the "live" tag must pass for app to be considered alive
                app.MapHealthChecks("/alive", new HealthCheckOptions
                {
                    Predicate = r => r.Tags.Contains("live")
                });
            }
    
            return app;
        }
    

ServiceDefaults projesi içerisindeki ‘Extensions.cs’ dosyasının içeriğini, faaliyetlerini ve kapsamını merak ediyorsanız eğer buradan tam olarak gözlemde bulunabilirsiniz.

Evet, böylece bizler mevcut olan bir mimariye .NET Aspire desteğini eklemiş ve artık hali hazırda kullanılabilir bir altyapı oluşturmuş bulunuyoruz. Artık bundan sonra .NET Aspire’ın mimariye kazandırdığı avantajlardan istifade etme zamanı olsa da bizler içeriğimizde bu aşamaya gelmeden önce bir de sıfırdan bir .NET Aspire projesinin nasıl oluşturulabileceğini inceleyeceğiz. Böylece altyapı oluşturmanın tüm yollarını değerlendirdikten ve kafalarda bu konuya dair bir soru işareti bırakmadıktan sonra .NET Aspire’ın kullanımına ve özellikle servisler arası efektif entegrasyon avantajlarına odaklanıyor olacağız. O halde buyurun devam edelim…

2. Sıfırdan .NET Aspire Projesi Oluşturmak

Şimdi sıfırdan bir projeyi direkt olarak .NET Aspire ile temellendirmek istiyorsak eğer bunun için bizlere aşağıdaki ekran görüntüsünde olduğu gibi (şimdilik) 7 farklı template sunulmaktadır;.NET Aspire Nedir Nasıl Kullanılır Detaylıca İnceleyelim...Bu template’leri tek tek izah etmemiz gerekirse eğer;

  • .NET Aspire Starter App
    Frontend’de Blazor ve backend’de de Web API olan hazır bir şablon sunmaktadır. İstenildiği taktirde Redis ile de cache’leme yapılabilmektedir.
  • .NET Aspire Empty App
    Boş bir .NET Aspire uygulaması oluşturan şablondur.
  • .NET Aspire App Host
    Yalnızca .NET Aspire Host uygulamasını yani orkestratör yapısını oluşturan şablondur.
  • .NET Aspire Service Defaults
    Benzer mantıkla yalnızca .NET Aspire ServiceDefaults projesini oluşturan şablondur.
  • .NET Aspire Test Project (MSTest)
    MSTest entegrasyon testlerini içeren bir .NET Aspire projesi oluşturur.
  • .NET Aspire Test Project (NUnit)
    NUnit entegrasyon testlerini içeren bir .NET Aspire projesi oluşturur.
  • .NET Aspire Test Project (xUnit)
    xUnit.net entegrasyon testlerini içeren bir .NET Aspire projesi oluşturur.

.NET Aspire Nedir Nasıl Kullanılır Detaylıca İnceleyelim...Bizler şimdilik boş bir .NET Aspire projesi(.NET Aspire Empty App) oluşturarak yolumuza devam edebiliriz. Yandaki görselden de görüleceği üzere bu oluşum neticesinde yukarıdaki satırlarda mevzu bahis olan projeler birebir aynı isim ve nitelikte oluşturulacaktırlar. Tek fark bu .NET Aspire zemininde kullanılmaya hazır projelerin olmamasıdır. Ee bu durumda yapılması gereken artık hangi servisler ve projelerle çalışma sergilenecekse onların ilgili solution’a eklenmesidir. Eee hocam AppHost projesinde bu servisleri referans etmeyecek miyiz? diye sorduğunuzu duyar gibiyim… Evet, edeceğiz ama manuel değil. Gerek yok 🙂 Visual Studio sağ olsun, .NET Aspire desteğinin olduğu solution’a yeni bir proje dahil edildiğinde aşağıdaki ekran görüntüsünde olduğu gibi otomatik olarak bunu AppHost projesinde referans etmekte ve bir yandan da o servise de ServiceDefaults projesini referans olarak eklemektedir..NET Aspire Nedir Nasıl Kullanılır Detaylıca İnceleyelim...

İşte, boş bir .NET Aspire projesi bu şekilde ayağa kaldırılmaktadır.

Şimdi kâh mevcut projede olsun kâh ilk başlangıçta olsun .NET Aspire altyapısının nasıl kurgulandığını detaylıca incelemiş bulunuyoruz. Artık yapısal olarak .NET Aspire’ı tanımaya geçebiliriz.

3. .NET Aspire Davranışsal Yapının İncelemesi

.NET Aspire desteği yürütülen bir çalışmada ne kadar proje olursa olsun önceki satırlarda bahsettiğimiz gibi tüm sorumluluk yani orkestrasyon AppHost projesindedir. Dolayısıyla öncelikle bu projenin servislerle arasındaki olan bağı incelemeli, nasıl bir çalışmayla entegrasyon süreçleri yönetilebilmekte, hangi metotları kullanmakta ve davranışsal olarak nelere dikkat edilmesi gerekmekte bunları değerlendirmeliyiz.

Şimdi ilk olarak servisleri .NET Aspire ile yapılandırırken en sade haliyle aşağıdaki gibi referans işlemi gerçekleştirilmektedir:

var builder = DistributedApplication.CreateBuilder(args);

builder.AddProject<Projects.ServiceA>("servicea");

builder.AddProject<Projects.ServiceB>("serviceb");

builder.Build().Run();

Görüldüğü üzere AddProject<T> metodu ile .NET Aspire’a bir proje bağlanabilmekte ve böylece ilgili projenin uygulamanın bir parçası olduğu ifade edilmektedir. Tabi sonraki süreçlerde bu referans edilen projeler üzerinden entegrasyon süreçlerini yönetebilmek için bunları aşağıdaki gibi de değişkenlerle referans edebilmekteyiz:

var builder = DistributedApplication.CreateBuilder(args);

var _servicea = builder.AddProject<Projects.ServiceA>("servicea");

var _serviceb = builder.AddProject<Projects.ServiceB>("serviceb");

builder.Build().Run();

Bu referans neticesinde artık bu projeler üzerinden entegrasyonları istediğimiz gibi yürütebiliriz. Şöyle ki; hatırlarsanız 1. adımda tarif ettiğimiz senaryoda ne demiştik, ServiceA, ServiceB‘ye http request’te bulunacak ve cevap alacaktır. İşte bu işlemin gerçekleştirilebilmesi için aşağıdaki gibi ServiceA‘nın ServiceB‘yi referans etmesi gerekecektir.

var builder = DistributedApplication.CreateBuilder(args);

var _servicea = builder.AddProject<Projects.ServiceA>("servicea");

var _serviceb = builder.AddProject<Projects.ServiceB>("serviceb");

_servicea.WithReference(_serviceb);

builder.Build().Run();

Burada WithReference metodu, servisler arasındaki service discovery bilgilerini ve connection string yapılandırmaları enjecte etmemizi sağlar. Yani bir nevi servisler arası fiziksel olmayan, mantıksal olan bir referans mantığı kurar. Bu referans bağımlılığı sayesinde servislerin nasıl etkileşime gireceği .NET Aspire seviyesinde tanımlanmış olur. Yani anlayacağınız ServiceA‘nın ServiceB‘den bir veri alması veya bir API çağrısında bulunabilmesi için WithReference metoduyla ilişkilendirmenin yapılması gereklidir.

İşte bu işlem neticesinde artık ServiceA‘dan aşağıdaki gibi herhangi bir host ve port bilgisini bilmeye gerek duymaksızın ServiceB tetiklenebilmektedir. Evet… Bu işlem nihayetinde bir Service Discovery’dir 🙂 (Her ne kadar içeriğimizin sonraki satırlarında Service Discovery’i
.NET Aspire üzerinden konsantre bir şekilde inceliyor olacak olsak da esasında buradakinden pekte farklı bir durum söz konusu olmayacaktır 🙂 )

ServiceA;

.
.
.
app.MapGet("/", async (HttpClient httpClient) =>
{
    var response = await httpClient.GetAsync("https://serviceb/api/data");
    response.EnsureSuccessStatusCode();
    var data = await response.Content.ReadAsStringAsync();
    return Results.Ok(data);
});
.
.
.

İşte tam da bu noktada akıllara şöyle bir sual gelmiş olabilir;

.NET Aspire ile uygulamayı geliştirmek için solution’daki tüm projelerin AppHost tarafından referans edilmesi gerekmektedir. Peki projeler tek bir solution altında değil de farklı solution’lara dağıtılmış büyük bir microservice mimarisindeyse ne yapacağız?

El-cevap; harici solution’lardaki projelerin .NET Aspire’a aktarılabilmesi için docker image’larından istifade edebilir ve böylece bu sorunu aşabiliriz. Burada aşağıdaki örnekte olduğu gibi AddContainer metoduyla container tabanlı harici bir servisi uygulamaya ekleme ve yapılandırabilme şansımız söz konusudur.

var builder = DistributedApplication.CreateBuilder(args);

var catalogApi = builder.AddContainer("catalog-api", "catalog-api")
                        .WithHttpEndpoint(targetPort: 8080)
                        .WithHttpHealthCheck("/health");

builder.AddProject<Projects.WebApplication1>("store")
       .WithReference(catalogApi.GetEndpoint("http"))
       .WaitFor(catalogApi);

builder.Build().Run();

Evet, bu yöntem sayesinde bizler geliştirme ortamlarında veya test aşamalarında belirli servisleri container olarak .NET Aspire sistemine dahil edip çalıştırabilme olanağı elde edebiliyoruz. Ancak bu yöntem neticesinde eklenen servislerde debugging yapılamayacağı aşikar olsa da yine de bizler ifade etmiş olalım. Ayrıca harici servislerin ServiceDefaults projesini referans olarak görmeleri tek koşuldur. Anlayacağınız .NET Aspire için mümkün mertebe tek bir solution tasarımında bulunulması en idealidir. Aksi taktirde süreç ister istemez türlü zahmetler gerektirebilir.

Velhasıl… Bu şekilde yapılandırıp .NET Aspire uygulamasını derleyip çalıştırdığımızda aşağıdaki görselde olduğu gibi bizleri bir Dashboard karşılayacaktır..NET Aspire Nedir? Nasıl Kullanılır? Detaylıca İnceleyelim...Bu dashboard’daki sekmelere hızlıca göz atarsak eğer;

  • Resources/Kaynaklar
    .NET Aspire uygulamasında referans edilen tüm projeler, servisler, api’ler ve RabbitMQ, Redis vs. gibi entegrasyonların container’larıyla birlikte bu kaynakların environment variables’ları gibi temel bilgileri bu sekmede listelenir.
  • Console/Konsol
    Uygulamadaki her projenin konsol çıktıları görüntülenir.
  • Structured/Yapılandırılmış
    .NET Aspire uygulamasındaki tüm projelerin ve container’ların logları filtrelenebilir bir şekilde listelenir.
  • Traces/İzlemeler
    Uygulamalarda yapılan tüm request’ler izlenebilir. Bu request’ler ister iç servisler arasında olsun, isterse de dış dünyayla haberleşme odaklı olsun fark etmemektedir. Misal olarak; ServiceA ServiceB‘ye, ServiceB‘de https://google.com‘a istekte bulunuyor diyelim. İşte bu sürecin trace’i aşağıdaki görseldeki gibi olacaktır..NET Aspire Nedir? Nasıl Kullanılır? Detaylıca İnceleyelim...
  • Metrics/Ölçümler
    Uygulamaların metrikleri grafiksel olarak listelenir.

Evet… Böylece yeterince .NET Aspire’ı davranışsal olarak incelediğimize göre artık Redis, PostgreSQL ve RabbitMQ gibi entegrasyonel çalışmalarında nasıl yürütüldüğünü incelemeye geçebiliriz.

4. .NET Aspire Entegrasyonları

.NET Aspire’ı avantajlı kılan önemli özelliklerinden birisi de Redis, PostgreSQL, RabbitMQ vs. gibi popüler servislerin entegrasyonunu kolayca yapabilmemize olanak tanımasıdır. Tabi bu olanağı üstteki satırlarda ifade etmeye çalıştığımız gibi ilgili servislere karşın üretilmiş NuGet paketleri aracılığıyla sağlayabilmektedir. Eğer ki .NET Aspire’a dair mevcut olan entegrasyonların listesini merak ediyorsanız bunun için AppHost projesine sağ tıklayarak aşağıdaki görselde olduğu gibi ‘Add’ -> ‘.NET Aspire package…’ kombinasyonunu takip edebilirsiniz..NET Aspire Nedir? Nasıl Kullanılır? Detaylıca İnceleyelim....NET Aspire Nedir? Nasıl Kullanılır? Detaylıca İnceleyelim...Şimdi burada entegrasyonlarla ilgili oldukça önemli bir farkındalık oluşturacağız. Entegrasyonlar genel anlamda hosting integrations/barındırma entegrasyonları ve client integrations/istemci entegrasyonları olarak ikiye ayrılırlar. Bu iki entegrasyon yapısı arasındaki farkı aşağıdaki tablo üzerinden mukayese edersek eğer;

Hosting Integrations/Barındırma Entegrasyonları Client Integrations/İstemci Entegrasyonları
Uygulamaları yapılandırmak için AppHost projesi içerisindeki kaynakları(servisler, projeler, container’lar ya da cloud resource’lar vs.) temsil eden entegrasyonlardır.

Misal olarak;
RabbitMQ’nun hangi servisler tarafından kullanılacağını AppHost içerisinde ifade edebilmek için tabi ki de hosting integrations yapmalı ve RabbitMQ’nun AppHost’a uygun olan Aspire.Hosting.RabbitMQ NuGet paketini kurmalıyız.

AppHost’ta yapılandırılmış kaynakların servislerdeki karşı yapılandırmasını sağlayan entegrasyonlardır.

Misal olarak;
AppHost içerisinde yapılandırılmış RabbitMQ’nun servisteki karşı yapılandırmasını sağlayabilmek için servise Aspire.RabbitMQ.Client NuGet paketini kurmalıyız.

.NET Aspire entegrasyon sürecinde hosting’e karşın client entegrasyonlarınının kullanılması best practices’dir. Yapılandırma hali ve iş mantığı gereği kimi hosting entegrasyonlarına karşılık client entegrasyonu bulunmadığını söylemekte fayda vardır. Hocam nedir bu entegrasyonlar diye sorarsanız akla direkt API Gateway gelmektedir… Ayrıca türüne göre entegrasyon kütüphanelerini gözlemlemek isteniyorsanız NuGet penceresinde aşağıdaki keyword’ler eşliğinde aramanın yapılması yeterlidir;

  • owner:Aspire tags:integration+hosting : Hosting entegrasyonlarını getirir. AppHost projesinde ‘.NET Aspire package…’ dendiğinde de yukarıda son paylaşılan görselde olduğu gibi varsayılan olarak bu keyword’le arama yapılan bir NuGet penceresi açılacaktır.
  • owner:Aspire tags:integration+client : Client entegrasyonlarını getirir. AppHost dışındaki herhangi bir servis üzerinden ‘.NET Aspire package…’ dendiğinde bu keyword ile arama yapılan NuGet penceresi açılacaktır.

Evet… Artık entegrasyon terminolojilerini ve mantığını kavradığımıza göre bazı popüler paketler üzerinden entegrasyon süreçlerini hususi olarak tecrübe etmeye başlayabiliriz.

5. .NET Aspire İle RabbitMQ Entegrasyonu

Hatırlarsanız 1. adımda tarif ettiğimiz senaryonun ikinci maddesinde ServiceB ise ServiceA‘ya message broker(RabbitMQ) üzerinden bir mesaj yollayacaktır. demiştik. Şimdi gelin senaryo gereği RabbitMQ ile .NET Aspire’da gerekli çalışmayı gerçekleştirmeye başlayalım.

RabbitMQ için öncelikle AppHost projesine bir hosting entegrasyonu(Aspire.Hosting.RabbitMQ) yapmamız gerekmektedir. Ardından ilgili message broker’ı kullanacak olan servisler için de ister client entegrasyonu(Aspire.RabbitMQ.Client) yapabiliriz, istersek de MassTransit üzerinden çalışma gerçekleştirebiliriz. Bizler MassTransit’i tercih edeceğiz.

İlgili projelere gerekli kütüphaneleri yükledikten sonra AppHost’ta aşağıdaki çalışmanın yapılması gerekmektedir.

var builder = DistributedApplication.CreateBuilder(args);

var username = builder.AddParameter("username", "gncy", secret: true);
var password = builder.AddParameter("password", "123", secret: true);
var rabbitMQ = builder.AddRabbitMQ("rabbitMQ", username, password)
    .WithManagementPlugin();

var _servicea = builder.AddProject<Projects.ServiceA>("servicea");

var _serviceb = builder.AddProject<Projects.ServiceB>("serviceb");

_servicea
    .WithReference(rabbitMQ)
    .WithReference(_serviceb);

_serviceb
    .WithReference(rabbitMQ);

builder.Build().Run();

Yukarıda servislerle RabbitMQ arasındaki orkestrasyon yapılandırmasını görüyoruz. Burada AddParameter metodu ile RabbitMQ’da kullanılacak kullanıcı adı ve parola bilgileri belirlenmekte, ‘secret’ parametresiyle de bu verilerin gizli/kritik veriler olduğu ifade edilmektedir. AddRabbitMQ metodu ile de .NET Aspire’a bir RabbitMQ entegrasyonu sağlanmaktadır. WithManagementPlugin metoduyla da RabbitMQ sunucusunu izlemek ve yönetmek için HTTP tabanlı bir API eklenmektedir. Yani bir nevi RabbitMQ’nun web tabanlı yönetim konsolu etkinleştirilerek kullanılabilir hale getirilmektedir. Ayrıca dikkat ederseniz RabbitMQ ile haberleşmeyi sağlayacak olan her iki servise de WithReference metoduyla RabbitMQ ilişkilendirilmektedir.

Ardından her iki servisin ‘appsettings.json’ dosyasında aşağıdaki konfigürasyonda bulunalım.

{
  .
  .
  .
  "ConnectionStrings": {
    "RabbitMQ": "amqp://gncy:123@localhost:5672"
  }
}

Ve servisleri de aşağıdaki gibi tasarlayalım:
ServiceA;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();
.
.
.
builder.Services.AddMassTransit(configurator =>
{
    configurator.AddConsumer<ServiceBSentMessageConsumer>();
    configurator.UsingRabbitMq((context, _configure) =>
    {
        _configure.Host(builder.Configuration.GetConnectionString("RabbitMQ"));
        _configure.ReceiveEndpoint("servicea-message-queue", e => e.ConfigureConsumer<ServiceBSentMessageConsumer>(context));
    });
});

var app = builder.Build();

app.MapDefaultEndpoints();
.
.
.
app.Run();

ServiceB;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();
builder.Services.AddMassTransit(configurator =>
{
    configurator.UsingRabbitMq((context, _configure) =>
    {
        _configure.Host(builder.Configuration.GetConnectionString("RabbitMQ"));
    });
});

var app = builder.Build();

app.MapDefaultEndpoints();
.
.
.
app.Run();

Ve uygulamayı derleyip çalıştıralım….NET Aspire Nedir? Nasıl Kullanılır? Detaylıca İnceleyelim...Görüldüğü üzere ‘rabbitMQ’ container’ı .NET Aspire sayesinde oluşturulmuş ve kullanıma hazır hale getirilmiştir. İşte .NET Aspire’ın konteyner mimarisini arka planda otomatik yönetmesinden kastettiğimiz bu otomatize davranışıdır. Evet, Docker Desktop üzerinden de göz atarsak eğer sırf bu çalışmaya odaklı bir rabbitMQ container’ının oluşturulduğunu gözlemleyebiliriz..NET Aspire Nedir? Nasıl Kullanılır? Detaylıca İnceleyelim...Yaptığımız çalışmayı test edebilmek için de ServiceB üzerinden https://localhost:7257/send-message adresini tetikleyerek message broker aracılığıyla ServiceA‘ya bir mesaj göndermeyi deneyebiliriz. Bu deneme neticesinde aşağıdaki ekran görüntüsünde olduğu gibi mesajların iletildiğini görebiliriz..NET Aspire Nedir? Nasıl Kullanılır? Detaylıca İnceleyelim...Ayrıca ayağa kaldırılmış bu uygulamayı sonlandırdığımızda, süreçte oluşturulmuş olan tüm container’ların silindiğini de göreceğiz.

6. .NET Aspire İle Keycloak Entegrasyonu

.NET Aspire ile Keycloak entegrasyonunu gerçekleştirebilmek için (Aspire.Hosting.Keycloak) hosting entegrasyonunu ve kullanılacağı servislerde de (Aspire.Keycloak.Authentication) client entegrasyonunu yapmamız gerekmektedir.

Misal olarak sadece ServiceA‘da Keycloak authentication sağlanacaksa AppHost’ta aşağıdaki yapılandırmada bulunulmalı;

var builder = DistributedApplication.CreateBuilder(args);

var username = builder.AddParameter("username", "gncy", secret: true);
var password = builder.AddParameter("password", "123", secret: true);
var rabbitMQ = builder.AddRabbitMQ("rabbitMQ", username, password)
    .WithManagementPlugin();

var keycloak = builder.AddKeycloak("keycloak", 1111, username, password);

var _servicea = builder.AddProject<Projects.ServiceA>("servicea");

var _serviceb = builder.AddProject<Projects.ServiceB>("serviceb");

_servicea
    .WithReference(rabbitMQ)
    .WithReference(keycloak)
    .WithReference(_serviceb);

_serviceb
    .WithReference(rabbitMQ);

builder.Build().Run();

ve ServiceA‘da da aşağıdaki çalışma gerçekleştirilmelidir.

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

.
.
.
builder.Services.AddAuthentication()
    .AddKeycloakJwtBearer("keycloak", realm: "example-realm", options =>
    {
        options.Audience = "aspire.api";
    });
.
.
.
var app = builder.Build();
.
.
.
app.Run();

Bu adımdan sonra Keycloak sunucusuna http://localhost:1111/ adresi üzerinden girerek belirtilen realm yapılandırmasında bulunulmalı ve gerekli Keycloak işlemleri yürütülmelidir.

7. .NET Aspire İle Redis Entegrasyonu

.NET Aspire’da Redis entegrasyonu için de (Aspirant.Hosting.Redis) hosting entegrasyonunun yapılması ve ardından AppHost’a aşağıdaki gibi Redis kaynağının eklenmesi gerekmektedir.

var builder = DistributedApplication.CreateBuilder(args);

var username = builder.AddParameter("username", "gncy", secret: true);
var password = builder.AddParameter("password", "123", secret: true);
var rabbitMQ = builder.AddRabbitMQ("rabbitMQ", username, password)
    .WithManagementPlugin();

var keycloak = builder.AddKeycloak("keycloak", 1111, username, password);

var redis = builder.AddRedis("redis");

var _servicea = builder.AddProject<Projects.ServiceA>("servicea");

var _serviceb = builder.AddProject<Projects.ServiceB>("serviceb");

_servicea
    .WithReference(rabbitMQ)
    .WithReference(keycloak)
    .WithReference(redis)
    .WithReference(_serviceb);

_serviceb
    .WithReference(rabbitMQ);

builder.Build().Run();

Ardından Redis’in kullanılacağı servisler de (Aspire.StackExchange.Redis) client entegrasyonu yapılmalı ve aşağıdaki gibi AddRedisClient metoduyla Redis servisi ilgili servislere eklenmeli.

var builder = WebApplication.CreateBuilder(args);

builder.AddRedisClient(connectionName: "redis");
.
.
.
app.Run();

Bu metot sayesinde Redis extension metotları dependency injection aracılığıyla IConnectionMultiplexer referansı üzerinden erişilebilir hale getirilmiş olacaktır. Bu aşamadan sonra ilgili servislerde aşağıdaki gibi çalışmalar sergilenebilecektir;

.
.
.
app.MapGet("cache-the-data", (IConnectionMultiplexer connectionMultiplexer) =>
{
    var db = connectionMultiplexer.GetDatabase(1);
    db.StringSet("name", "gncy");
});

app.MapGet("get-the-data-fron-the-cache", (IConnectionMultiplexer connectionMultiplexer) =>
{
    var db = connectionMultiplexer.GetDatabase(1);
    var name = db.StringGet("name");
    if (!string.IsNullOrEmpty(name))
    {
        return name.ToString();
    }
    return "";
});

8. .NET Aspire İle SQL Server Entegrasyonu

SQL Server entegrasyonu için de (Aspire.Hosting.SqlServer) kütüphanesiyle hosting entegrasyonu yapılmalı ve (Aspire.Microsoft.Data.SqlClient) kütüphanesiyle de client entegrasyonu sağlanmalıdır. Ayrıca Entity Framework Core kullanılacaksa da (Aspire.Microsoft.EntityFrameworkCore.SqlServer) kütüphanesiyle client entegrasyonu desteklenmelidir.

Velhasıl… Akabinde AppHost’a aşağıdaki gibi SQL Server kaynağı eklenmelidir.

var builder = DistributedApplication.CreateBuilder(args);

.
.
.
var sqlServer = builder.AddSqlServer("sqlServer")
    .WithLifetime(ContainerLifetime.Persistent);
var database = sqlServer.AddDatabase("exampleDB");

var _servicea = builder.AddProject<Projects.ServiceA>("servicea");
.
.
.
_servicea
    .
    .
    .
    .WithReference(database);
.
.
.
builder.Build().Run();

Burada görüldüğü üzere AddSqlServer metoduyla SQL Server kaynağı eklenmekte ve bir yandan da veritabanı olduğu için kalıcılık bekleneceğinden dolayı WithLifetime metoduyla bu container’ın sürekli olarak çalışması sağlanmaktadır. Böylece .NET Aspire tarafından tüm container yapılanması otomatik yönetilirken, uygulama kapatıldığı taktirde .WithLifetime(ContainerLifetime.Persistent) şeklinde yapılandırılanlar kalıcı olarak çalışmaya devam edecektirler. Ayrıca oluşturulan SQL Server kaynağına bir veritabanı ekleyebilmek için de AddDatabase metodundan istifade edilmektedir.

Ardından aşağıdaki gibi AddSqlServerDbContext metodu eşliğinde SQL Server dependency’leri istenilen servise entegre edilebilir;

.
.
.
builder.AddSqlServerDbContext<ExampleDBContext>(connectionName: "exampleDB");
.
.
.

9. .NET Aspire İle Service Discovery Nasıl Yapılır?

.NET Aspire’da AppHost üzerinde yapılan kaynak tanımları, özünde Web API’ler için de bir service discovery yapılandırması sağlamaktadır. Şöyle ki;

var builder = DistributedApplication.CreateBuilder(args);
 
var _servicea = builder.AddProject<Projects.ServiceA>("servicea");
 
var _serviceb = builder.AddProject<Projects.ServiceB>("serviceb");
 
_servicea.WithReference(_serviceb);
 
builder.Build().Run();

Yukarıdaki kod bloğunu ele alırsak eğer ServiceA, ServiceB‘ye WithReference metoduyla ilişkilendirildiği için ‘serviceb’ ismi üzerinden host ve port bilgileriyle ilgilenmeksizin rahatlıkla istekte bulunabilir. Yani anlayacağınız .NET Aspire sayesinde servisler arasındaki bağımlılıklar bu şekilde, kolayca yönetilebilmektedir.

10. .NET Aspire Uygulamalarını Deploy Etme

.NET Aspire’ın büyük avantajlarından bir diğeri ise çeşitli cloud ortamlarında deploy süreçlerini oldukça basitleştirmesidir. Bir .NET Aspire uygulamasını cloud’a deploy edebilmek için öncelikle AppHost’ta tanımlanmış olan kaynakları özetleyecek bir JSON dosyası oluşturmamız gerekmektedir. Terminolojik olarak bu dosyaya manifest file(bildirim/beyan dosyası) denmektedir. Bunun için solution dizininde aşağıdaki talimattan istifade edebiliriz.

dotnet run –project NetAspire.Example.AppHost\NetAspire.Example.AppHost.csproj –publisher manifest –output-path ../aspire-manifest.json

Bu talimat neticesinde ilgili dizinde aspire-manifest.json adında bir manifest dosyası oluşturulacaktır. Bu dosyanın proje yapımıza göre nasıl bir içeriğe sahip olduğunu merak ediyorsanız şuradan inceleyebilirsiniz.

Velhasıl kelam, artık uygulamayı cloud’a deploy edebiliriz. Bizler şimdilik örneklendirmeyi Azure üzerinden gerçekleştireceğiz. Bunun için Azure Developer CLI (azd) component’ine ihtiyacımız olacaktır. Bu component, uygulamanın local development ortamından Azure’a kolaylıkla deploy edilmesi için kullanılan open source bir araçtır. Bu aracın yüklenmesi için terminal üzerinden aşağıdaki talimatın verilmesi yeterlidir;

winget install microsoft.azd

azd’yi yükledikten sonra artık deploy’u başlatabiliriz. Bunun için de uygulamanın AppHost projesinin dizinine odaklı bir terminal üzerinden aşağıdaki talimatın verilmesi gerekmektedir;

azd init

Bu talimat neticesinde sorular soracaktır. Use code in the current directory sekmesinden devam ediniz. Günün sonunda aşağıdaki ekran görüntüsünde olduğu gibi azd ‘./azure.yaml’ ve ‘./next-steps.md’ isminde bir dizi dosya oluşturacaktır. .NET Aspire Nedir? Nasıl Kullanılır? Detaylıca İnceleyelim...Bu dosyaların yapısal davranışlarını izah etmemiz gerekirse eğer;

  • azure.yaml
    Uygulamanın Azure kaynaklarına nasıl deploy edileceğini tanımlayan bir yapılandırmaya sahiptir. İçerisinde oluşturulacak kaynaklar (örneğin; Azure App Services, Azure Functions, Databases vs.) yapılandırma ayarları ve deploy süreçleri bulunur.
  • next-steps.md
    Bu dosya ise, deploy sonrası yapılacakların ve izlenmesi gerekenlerin rehberini içermektedir. Misal olarak, uygulamanın nasıl çalışacağı, nasıl test edileceği ve yönetileceği hakkında bilgiler içerir.

Evet, artık deploy’u gerçekleştirebiliriz. Bunun için aşağıdaki talimatın verilmesi gerekmektedir;

azd up

Tabi bu talimat neticesinde ERROR: not logged in, run `azd auth login` to login şeklinde bir hata alırsanız, anlaşılan o ki azd üzerinden oturum gereksinimi söz konusudur diyebiliriz. Bunun için de azd auth login talimatıyla oturum açabilir ve ardından tekrar yukarıdaki talimatı verebilirsiniz.

Devamında ise azd, Azure tarafında deploy’un yapılacağı bir subscription ve konum bilgisi isteyecek ve ardından deploy süreci başlatılacaktır. Tabi bundan sonrası şuradaki adımlar eşliğinde hesaba özel şekillenecektir diyebiliriz. Ayrıca deploy sürecine dair tam teferruatlı bilgi için de burayı inceleyebilirsiniz.

Artık uygulamayı nihayete erdirmeyi ve testleri sizlere bırakıyorum…

Böylece geniş çaplı olarak .NET Aspire’ı incelemiş ve yer yer pratiksel dokunuşlarla kısmen tecrübe etmiş bulunuyoruz. Şahsen son zamanlarda yaptığımız özel bir yazılım sürecini .NET Aspire kullanarak şekillendirmeye çalıştığım için oldukça etkileyici bulduğumu söyleyebilirim. Özellikle konteyner yapılanmasını direkt üstlenmesi, console yapılanmasını servis odaklı daha erişilebilir hale getirmesi ve bir yandan da dahili olarak gözlemlenebilirlik ve izleme araçlarını direkt hazır sunabilmesi benim için favori özellikleridir diyebilirim. Tabi henüz hızlı entegrasyon yapılanmasını tam değerlendirebileceğim bir çalışmam olmadığı için heyecanla bunu da deneyimlemeyi beklediğimi itiraf ediyor, eşlik ettiğiniz için teşekkür ediyorum…

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not : Örnek çalışmaya aşağıdaki github adresinden erişebilirsiniz.
https://github.com/gncyyldz/NetAspire.Example

Bunlar da hoşunuza gidebilir...

10 Cevaplar

  1. hamit dedi ki:

    Farklı solution altında geliştirilen projeleri tek bir aspire dashboard altında toplayabilir miyiz ?

  2. Faruk dedi ki:

    faydali bir makale tesekkurler hocam

  3. İlker Keklik dedi ki:

    Aciklayici bir makale olmus tesekkur ederim. Daha realistik bir calisma yaparken .Net Aspire in yonetimi oldukca zor olmali. Farkli solutionlardaki projeleri .Net Aspire entegrasyonuna tabii tutmak ve deploy almaktansa geleneksel yonetmlerin tercih edilmesi, Aspire’ in su anki durumunu dusunursek daha uygun olacaktir kanisindayim. Bu konuda goruslerinizi paylasirsaniz sevinirim Gencay Hocam.

    • Gençay dedi ki:

      Evet katılıyorum. Farklı solution’larla uğraşmak bence de şimdilik pek iç açıcı gözükmemekte.

      Bol faydalar olsun.

  4. Ahmet dedi ki:

    Teşekkürler. Harika bir makale olmuş. Elinize emeğinize sağlık

  1. 03 Şubat 2025

    […] Gençay Yıldız: https://www.gencayyildiz.com/blog/net-aspire-nedir-nasil-kullanilir-detaylica-inceleyelim/ […]

Bir yanıt yazın

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