Arda Çetinkaya Yazılım ve arada kendim ile ilgili karaladıklarım…

Günümüz çözümlerinin çoğunlukla servis odaklı çözümler olması, bu çözümlerin barındığı ağ(network) alt yapılarının da özenle tasarlanması ve belli sınırlarının çizilmesi konularını gündeme getiriyor. Özellikle güvenlik öncelikli olarak beklenen bir kalite ölçütüyse… “Cloud” platformlar üzerinde çözümler geliştiriyorsak, platformun bu konular için sunduğu hizmetleri de kullanmak tahmin edersiniz ki oldukça önemli ve gerekli. Daha çok Azure platformunda çözümler geliştiren/sunan biri olarak bu açıdan iki hizmetten bahsetmeye çalışacağım bu yazıda; Azure’daki Private Endpoint ve Service Endpoint

Azure üzerinde Paas(Platform as a service) şeklinde sunulan hizmetleri kullanan çözümler geliştirdiğimizde, bu servisler ile olan iletişimin belli kısıtlamalar ve sınırlar içinde olması çözümün güvenilirliği adına önemli. Belli servis erişimleri için kullanılan anahtarlar ya da kimlik bilgilerinin sızması durumunda ağ alt yapılarındaki tasarımlar ve kısıtlamalar oluşabilecek riskleri de minimuma indirgemek için oldukça değerli.

Service Endpoint

Bir Azure Virtual Network yarattığımız zaman, bu sanal ağ içerisinde olan kaynaklar; mesela bir tane sanal makine(VM) diğer Azure servisleri ile bir “Public IP” üzerinden, internete çıkarak iletişim kurar. Ama peki bunu istemezsek?

Oluşturulan sanal ağlarda(virtual network) tanımlanabilen “Service Endpoint”ler ile sanal ağ içerisinde oluşturulan kaynakların servislere erişiminin, sanal ağ tanımına göre sahip olduğu “Private Ip” ile olması mümkün olabiliyor. Bu sayede internete çıkmadan diğer Azure hizmetine erişimi sağlanabilmekte. İnternete çıkmadan Azure içindeki temel sanal ağ içinde kalarak erişimin sağlanması hem güvenlik hem de performans açısından önemli bir getiri.

“Service Endpoint”leri, ağdaki gizli kaynakların bir Azure hizmetine tanıtılması gibi düşünebiliriz. “Service Endpoint”ler sayesinde, Azure hizmeti bir ağdaki bileşeni tanımış ve ona özel bir kapı açmış gibi canlandırmak belki daha basit olacak. Azure portal üzerinden bu tanımlamaları oldukça kolay bir şekilde yapabiliyoruz.

Aşağıdaki Azure hizmetlerini bir “Service Endpoint” olarak eklemek mümkün.

  • Microsoft.AzureActiveDirectory
  • Microsoft.AzureCosmosDB
  • Microsoft.CognitiveServices
  • Microsoft.ContainerRegistry
  • Microsoft.EventHub
  • Microsoft.KeyVault
  • Microsoft.ServiceBus
  • Microsoft.Sql
    • SQL Database
    • MySQL
    • PostgreSQL
    • Synapse Analytics
  • Microsoft.Storage
  • Microsoft.Web (App Services)

Hem küçük bir ek olması, hem de daha iyi anlaşılması için; sanal ağa “Service Endpoint” tanımı yapmadan, direkt erişeceğimiz Azure hizmeti üzerinden de “Service Endpoint” aktifleştirmesini yapabiliyoruz. Zaten direkt bir hizmeti sanal ağımıza eklemek istediğimizde portal üzerinden önce sanal ağda “Service Endpoint”in aktifleştirilmesinin yapılması gerekliliğini görüyoruz.

Dan diye birden SQL Server’dan da örnek verdim ama biraz daha canlandırabilmek için örneği daha net ele alalım; bir sanal ağ içinde bir “subnet”e bağlı da bir VM olduğunu düşünelim. Bu VM’i internete açmadığımızı yani “Public IP”sinin de olmadığını düşünelim. VM içindeki uygulamamız Azure SQL veri tabanına erişmek durumunda olsun. Eğer sanal ağdaki bu ilgili “subnet”‘de Microsoft.Sql için bir “Service Endpoint” tanımı yoksa, VM içindeki uygulamamız SQL veri tabanına erişemeyecektir. Ancak “Service Endpoint” tanımladığımız zaman VM, iletişimi tamamen Azure ağ alt yapısı içinde kalacak şekilde SQL veri tabanına erişebilecektir. İnternette dolaşmadan da erişimin sağlanıyor olması veri güvenliği açısından da oldukça önemli.

Ancak burada önemli bir nokta var ki; o da Azure SQL veri tabanı hizmetine erişirken, Azure “Public DNS”si üzerinden erişim sağlanıyor. Yani küresel anlamda Azure tarafında bir DNS sorunu olursa uygulamamız veri tabanına erişemeyecektir.

Peki, Private Endpoint ne?

“Private Endpoint”, “Service Endpoint”e benzeyen bir kavram; onun bir üst versiyonu gibi ele alabiliriz. “Private Endpoint”ler Azure Private Link hizmetninin temel bir parçası. Azure Private Link‘de, sanal ağ kaynaklarının diğer servislere erişiminin yine aynı sanal ağ içinde olması sağlanıyor ama ek olarak diğer servislerin de sanal ağ içinden verilen özel bir IP ile erişilmesi sağlanıyor. Bu özel IP alabilmesi hizmeti de “Private Endpoint” olarak adlandırılıyor.

Devam…

C#’daki “[Attribute]” yani nitelik yaklaşımı dilin yorumlanması açısından güzel getirilere sahip bir özellik. Geliştiricinin, derleyiciye belli üst bilgileri(a.k.a metadata) aktararak, kodun derlenme ya da çalışma şeklini değiştirebiliyor olması yazılım çözümlerine ve yazılım geliştirme yöntemlerine çok büyük artılar sağlayabiliyor.

Genellikle .NET platformunun sunduğu hazır “[Attribute]”’ları kullanarak çözümlerimizi geliştiriyoruz; gerektiği yerlerde kendi özel [Attribute]’larımız ile de çözümlerimizi genişletebiliyoruz. Açıkçası çok derinlere girmeyeceğim, -ki zaten ihtiyaçlara göre sık sık tercih edebiliyoruz. Ama [Attribute]’ların öneminin ve değerli bir özellik olduğunun altını koyu bir kalemle altını çizmek isterim. Bu önemi ve farkındalığı biraz daha arttırabilmek için, faydalığı olduğunu düşündüğüm birkaç [Attribute]’dan hızlıca bahsetmeye çalışacağım. Hem [Attribute]’lar ile neler yapılabilir yeni fikirlerler vermesi, hem de farkında olunmayan birkaç niteliğin farkındalığı ile yazılım çözümleri için faydalı olur umarım bu yazı. Tabi ki bilenler, kodunda kullananlar illa ki vardır; onlar için biraz tekrar olacak belki ama en azından yorumlarını paylaşabilirlerse ekstra sevinirim, seviniriz. Değil mi?

Nitelikler(a.k.a [Attribute]), .NET ve Rosyln‘nın da gelişmesi ile statik kod analizi açısından da önemli bir yere geldi. Bahsetmeye çalışacağım bir tanesine bu açıdan da bakmakta fayda var.

[CallMemberName]

Bir metodu hangi metot çağırmış bunu bilebilmek kod içerisinde bazen ihtiyaç olabiliyor. En basitinden Log ve Trace ihtiyaçları için önemli. Kodun içerisine statik olarak belirtmek yerine(-ki hepimizin yapmışlığı vardır) [CallMemberName] niteliğiyle bir parametre ekleyerek bunu yapmak mümkün.

public class SomeGreatAPI
{
    public void Foo()
    {
        this.Log("Invoked in some method,which one?");
    }

    private void Log(string message, [CallerMemberName] string memberName="")
    {
        Console.WriteLine($"{message}");
        Console.WriteLine($"This message is from {memberName}");
    }
}

[ModuleInitializer]

Belki de çok gerekli gelmeyecek ama belli senaryolar için önemli olabilecek [ModuleInitializer] niteliğinden de bahsetmek isterim. Bu nitelikle işaretlediğimiz bir metot, ilgili olan “assembly” ilk yüklenirken çalışacak metot oluyor. Belli iş kurallarının kontrol edilmesi ya da ilk önce bazı değerlerin oluşturulup ayarlanması gibi senaryolar için güzel bir özellik. Tabi ki başka yaklaşımlarla da bu tarz bir ihtiyacı kısmen de olsa gerçekleştirebiliriz ama hem kodun derli toplu olması için hem de framework’ün kendi içindeki ideal bir yapı olduğu için tercih edilebilecek güzel bir ifade şekli diye düşünüyorum.

namespace Csharp10.Features
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

public class SomeGreatAPI
{
    [ModuleInitializer]
    public static void Init()
    {
        Console.WriteLine("Initialize...");
    }
}

Yukarıdaki basit örneği çalıştırdığımız zaman aşağıdaki gibi bir çıktı kodun daha iyi anlaşılmasını sağlayacaktır.

Initialize...
Hello, World!

Kod içerisinde [ModuleInitializer] ile belirttiğimiz metot ilk çalışacak metot. Dolayısıyla uygulama ilk çalıştığında belli kontroller ya da doğrulamalar yapmak için güzel bir fırsat. [ModuleInitializer] niteliği ile işaretleyebilmemiz için metodun birkaç gereksinimi var; parametre almayan, dönüş değeri olmayan “static” bir metot olması gibi…

[return:NotNullWhen(bool returnValue)]

Bu nitelik biraz değişik; API/SDK tarzı bir çözüm sunuyorsanız ya da bir Framework geliştiriyor ve bunu yazılımcılara sunuyorsanız, kullanan kişilere yol göstermesi için güzel bir nitelik diye düşünüyorum.

public class SomeGreatAPI
{
    [return: NotNullIfNotNull("email")]
    public static string? GetEMailProvider(string? email)
    {
        //return SomeOperationResult
    }
}

Yukarıdakine benzer bir metodu sunduğumuzu düşünelim. “Nullable” bir metot olup, aldığı parametre değerlerine göre null dönmesi gereken bir ihtiyaç olduğunu hayal edelim. E-Posta adresinin hangi servis sağlayıcıdan olduğunu bulmamız gereksin mesela… @gmail.com, @live.com gibi. Eğer e-posta parametresi verildiyse mutlaka bir değer dönsün, eğer parametre verilmediyse null dönsün. Bu metodu kullanacak geliştiriciler için bu mantığı [return:NotNullIfNotNull(parameterName)] niteliği ile anlatmak mümkün. Metodu aşağıdaki gibi normal bir şekilde çağırdığımızı düşünelim.

Her şey normal gözükecektir. Derleme adımında her hangi bir uyarı bile gözükmeyecektir. “nullable” olan email parametresini metodu çağırdığımız yerdeki iş kuralından dolayı doldurmadığımızı düşünelim.

Visual Studio direkt olarak “provider” değişkeninin altını çizecektir ve uyarı gösterecektir. Ya da zaten derlendiğimizde de kodu uyarı olarak bunu göreceğiz.

“Eeeee şimdi zaten null kontrolü yapılmalı” diye düşünebilirsiniz; -ki evet, belli kontrollerin daha güvenli kod yazmak için yapılıyor olması gerekli. Tecrübenin yanında, bu tarz bilgilendirmeler ile API’yı kullanmaya başlayan kişileri doğru bir şekilde bilgilendirmek mümkün. Ayrıca daha düzenli kod yazmak için de tercih edilebilecek bir özellik.

.NET Framework ile uzun süre çalışmış kişiler için bu yaklaşım biraz tanıdık gelecektir belki ve “Code Contracts”‘ı anımsatacaktır. .NET Framework’de kodların çalışma mekanizması içinde ön koşul ya da son çalışma adımı gibi ara tanımlar koyabiliyorduk. Ne yazık ki yeni .NET platformunda(>=5.0) şu an için pek mümkün değil. Ama az önce bahsettiğim nitelik(ler) ile benzer koşulları oluşturmak mümkün. Bu arada bu niteliğin sağlıklı çalışabilmesi için projede nullable özelliğinin aktif olması gerekmekte. Benzer başka nitelikler de .NET’in içinde mevcut. Merak uyandırdıysa eğer, onlara da göz atmanızı tavsiye ederim.

Bu yazı biraz kısa ve hızlı oldu ama “[Attribute]”ler konusunda farklı yaklaşımlar geliştirmek için biraz olsun motivasyon sağlar. Yakın zamanda bazılarının faydalarını gördüğüm için kendi adıma daha kalıcı olması için hızlıca paylaşmak istedim. Bir sonraki yazıda görüşmek üzere, mutlu kodlamalar… 😀😀

ASP.NET Core tarafında farkında olunduğu zaman değişik ihtiyaçlar için çözüm olabilecek ama çok fazla gündeme gelmeyen çeşitli API’lar mevcut. Bunlardan biri de IHostingStartup ara yüzü. Geliştiriciler için SDK/Framework/eklenti tarzı araçlar geliştiriyorsanız ya da uygulama başlarken bazı kontroller yapmanız gerekiyorsa faydalanabileceğiniz bir ara yüz. Hızlıca ve kısaca ne olduğunu, neden tercih edilebilir bahsetmeye çalışacağım.

Ama önce bir hatırlatma…

Bildiğiniz gibi ASP.NET Core uygulamaları “host” kavramı ile çalışır. Bu “host” yaklaşımı uygulamanın başlamasını ve yaşam döngüsünün yönetilebilmesini sağlar. ASP.NET Core uygulamalarındaki standart proje şablonlarındaki Program.cs içeriğinden hatırlayabiliriz.
Bir host(IWebHostBuilder) yaratıyoruz, bunu konfigürasyon ile ayarlıyor, gerekli servisleri ekliyor ve uygulamanın yaşam döngüsünü oluşturuyoruz.

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((hostingContext, config) =>
            {

            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.ConfigureServices(services =>
                {
                    services.AddRazorPages();
                })
                .Configure(app=>
                {
                    var env = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
                    if (env.IsDevelopment())
                    {
                        app.UseDeveloperExceptionPage();
                    }
                    else
                    {
                        app.UseExceptionHandler("/Error");
                        app.UseHsts();
                    }

                    app.UseHttpsRedirection();
                    app.UseStaticFiles();

                    app.UseRouting();

                    app.UseAuthorization();

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

Ama tabi biraz da düzenli olması adına, ek bir Startup.cs içeriği ile bu yapıyı farklı bir sınıf içerisinde, UseStartup() metodu ile kullanabiliyoruz, -ki zaten standart proje şablonlarından hep bu şekilde geldiği için bu yönteme daha aşinayız. Ama genel ASP.NET Core’daki yapıyı bilmek önemli diye düşünüyorum.

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
    }

Bu küçük hatırlatma ile aslında paylaşmak istediğim konuya giriş için kapıyı açmış oldum. Bütün bu işlemlerden önce uygulamadan ayrı bir şekilde bazı işlemleri ASP.NET Core’da nasıl yapabiliyoruz bir bakalım.

IHostingStartup

IHostingStartup arayüzü, ASP.NET Core’da IWebHostBuilder ile “host” oluşturulurken, öncesinde bazı işlemleri yapabilmek için tercih edebileceğimiz bir yöntem. Bu özelliğin bir güzel yanı, mevcut bir ASP.NET Core uygulamasından bağımsız ayrı bir *.dll üzerinden bazı işlemleri yapabilmek; belli konfigürasyonları uygulamaya eklemek ya da belli işlemleri gerçekleştirmek gibi.

Biraz daha net anlaşılması için şöyle örnekler verebilirim; lisans kontrolü, uygulamanın aktivasyonu, uygulama çalışmadan önce bazı ayarların sistem seviyesinde yapılması ve kontrolü(belli dizinlerin yaratılması, belli dosyaların yedeklenmesi…gibi) ya da uygulamanın sağlıklı çalışması için bazı taşıma-dönüşüm işlemlerinin önceden yapılması…
Eee zaten yapabiliriz, yapılabilir böyle şeyler” diyenler olacaktır. Tabi ki bir şekilde bu tarz gereksinimleri gerçekleştirebiliriz ama neden bunları SDK’nın sağladığı yaklaşımlar ile daha kolay ve .NET için en uygun şekilde yapmayalım.

Yavaştan kod tarafına geçelim ki biraz daha netleşmeye başlasın. Visual Studio’nun standart proje şablonlarından ASP.NET Core Web App(StartupDemos) ve bir tane de Class Library(HostingStartupExternalLibrary) kullanacağım. Class Library, bizim dışardan uygulamaya dahil edeceğimiz, IHostingStartup ara yüzünden türeteceğimiz yapıyı barındıracak; yazının asıl olay yani. ASP.NET Core Web App ise, sadece örnek amaçlı, dışardan ekleyeceğimiz IHostingStartup‘ı kullanacak uygulama.

HostingStartupExternalLibrary projesine bakalım önce. IHostingStartUp arayüzünden yarattığımız tek bir sınıfımız var. Bu arayüzün sağlamış olduğu Configure(IWebHostBuilder) metodu ile WebHost’umuzu ayarlayabiliyoruz. Ben burada örnek olsun diye; bir Web API*‘sine çağrı yapıp dönen değerleri WebHost’un konfigürasyonunda kullanılması için ayarlıyorum. Çok saçma biliyorum ama başka bileşenler/sistemler ile iletişim de kurup, farklı iş ihtiyaçlarını gerçekleştirebilmek adına örnek olabilir. Mesela yazının başında bahsetmiş olduğum senaryolardan; uygulama çalıştığında bir aktivasyon, bu tarz bir Web API çağrımı ile gerçekleşebilir.

*Burada örnek olması adına bu tarz örnekler ve denemeler için kullanılabilecek ücretsiz basit bir Web API kullandım.

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using System.Net.Http;

[assembly: HostingStartup(typeof(HostingStartupExternalLibrary.SomeExternalHostingStartup))]
namespace HostingStartupExternalLibrary
{
    public class SomeExternalHostingStartup : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            HttpClient client = new();
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Add("User-Agent", "SomeSpecificCustomer");
            string boredAPIResult = client.GetStringAsync("https://www.boredapi.com/api/activity").Result;
            var activity = System.Text.Json.JsonDocument.Parse(boredAPIResult);

            builder.ConfigureAppConfiguration((host, config) =>
            {
                var externalSettings = new Dictionary<string, string>
                {
                    {"EXTERNAL_STARTUP_ACTIVITY", activity.RootElement.GetProperty("activity").GetString()},
                    {"EXTERNAL_STARTUP_TYPE", activity.RootElement.GetProperty("type").GetString()},
                    {"EXTERNAL_STARTUP_KEY", activity.RootElement.GetProperty("key").GetString()}
                };

                config.AddInMemoryCollection(externalSettings);
            });
        }
    }
}

Burada dikkat ederseniz, IWebHostBuilder üzerinden konfigürasyonlara belli değerleri ekliyoruz. Bu IHostingStartup arayüzünü uygulamadığımız Web uygulamasında, uygulamadan bağımsız olarak bu konfigürasyon değerlerine ulaşabileceğiz. Birazdan oraya da geleceğiz.

Devam…

.NET dünyasında son 5-6 senedir yaşanan büyük değişikler malum; birden fazla farklı işletim sistemlerinde çalışabilmesi, açık-kaynak yaklaşımlar ile geliştirilmesi, birleşik bir kod tabanı üzerinde sunulan geniş API özellikleri, yazılım geliştirme dünyasında oldukça sıcak konular.

.NET Framework ve .NET Runtime şeklinde hayatımıza giren kavramlardaki değişiklikleri ve yaklaşımları, mevcut durumu ve yol haritasını baz alarak basitçe anla(t)maya, kendimce yorumlamaya çalışacağım. Bazen sorulan sorularda, muhabbetlerde, .NET ile yeni tanışanlarda ya da uzun süredir .NET ile çalışmış ama yenilikleri çok takip edememiş kişilerde kavram karışıklıkları gördüğüm (ya da öyle sandığım) için böyle bir paylaşımda bulunmak istedim. .NET’i daha iyi anlamak geliştirilen çözümlerin kalitesini olumlu yönde de etkileyeceği için umarım faydalı olur.

.NET artık sadece bir Framework değil…

.NET kavramları artık sadece bir “framework” ya da “runtime” yaklaşımı ile yorumlanacak kadar yalın değil. Böyle bir başlıkla yazıyı paylaşmamın sebebi de biraz bu. 😀 Artık .NET kendi içinde bir eko-sistemi olan bir platform. Günümüzün gerektirdiği sistemlerde (web, “cloud”, mobil cihazlar, masaüstü, IoT cihazlar… gibi) çözümler geliştirmek için tercih edilebilecek bir platform. Bu platformun sağladığı API’lar, run-time’lar, framework’ler, araçlar geliştiriciler için kolaylıklar ve imkanlar sağlıyor.

Genel yazılım geliştirme eko-sistemindeki gelişmeler, yazılım geliştirme araçlarının çeşitlenmesi ve yeni fırsatların ortaya çıkması .NET için böyle bir değişikliğin gerekliliğini gösteriyordu zaten. Büyük resme biraz daha yukarıdan bakabilmek ve o şekilde hareket etmek güzel yaklaşım. Bu yüzden Microsoft’un, .NET’e olan bu yeni yaklaşımını bir yazılımcı olarak memnuniyetle karşılıyorum.

Değişim ve adaptasyon

Bulunduğumuz çağda “hızlı” değişim ve “adaptasyon” yazılım çözümlerinin sağlaması gereken en önemli özellik diye düşünüyorum. Bir yazılımın ne kadar uzun süre yaşayabildiğinden çok, yazılımın ne kadar hızlı değişim yaşayabiliyor olması ve yeniliklere adaptasyon sağlaması artık daha önemli. Dolayısıyla değişime ayak uydurmak için .NET platformu da artık daha hızlı değişiyor ve gelişiyor diye düşünüyorum. 2-3 yıl boyunca platform içinde olan bir kavram, 5. yılında belki baştaki desteği sunamayacak, daha farklı ve etken bir alternatifi sunabilecek. Hatta belki de daha da kısa zaman aralıklarında… Alışa gelmiş yazılım geliştirme yaklaşımlara göre belki, “Olur mu ya öyle, niye artık yok. O nasıl kaldırılıyor?” gibi söylemlerde bulunabiliriz belki. Ama artık günümüzde ne kadar geçerli bir bakış açısı bilemiyorum. Geliştirmelerin “topluluk” odaklı ve açık-kaynak odaklı bir şekilde yapılıyor olmasına ek, böyle bir bakış açısından dolayı da .NET’in ara sürümler(preview, release candidate) ile daha sık karşılaşıyoruz, karşılaşacağız. Tabi ki bu ara sürümleri, üretim ortamlarında kullanmak çok sağlıklı değil(?) ama yayınlanacak sürümleri anlamak, gelişmelere adaptasyon sağlamak için bu ara sürümler üzerinde de çalışabilmek önemli.

Microsoft, artık .NET’in çift sayı olan versiyonlarına “Long-term support(LTS)” yani daha uzun süreli destek sağlayacak. Belli versiyonlara olan desteğin daha kısa süreli olacağından, aslında sürekli “güncel” kalabilmeyi desteklemek adına güzel bir hareket.

Devam…

Uygulama geliştirirken kullandığımız dil dışında, üzerinde çalıştığımız “framework” ya da “kütüphane” API‘larına da hakim olmak, bazı gereksinimleri kolay bir şekilde sunabilmek ya da kullandığımız “framework”‘ü daha etkin kullanabilmek için oldukça önemli diye düşünüyorum. Bu yaklaşımla ASP.NET Core içindeki küçük bir API’dan bahsetmeye çalışacağım; -ki farklı senaryolar için ihtiyaç duyulabilecek güvenli ya da limitli veri modelleri oluşturmak için faydalı olabilir.

Sadece belirli bir süre için geçerli olacak veri modelleri ya da “text” ifadeler zaman zaman ihtiyacımız olan bir yaklaşım olabiliyor. Üyelik işlemlerinde “E-mail onayı” için gönderilen linkler ya da şifre yenilemek için gönderilen linkler sanırım birçok kişiye tanıdık gelecektir. Ya da “soft-OTP”(One-time password) senaryolarında belli bir zaman geçerli olacak kodlar ya da “Bearer” token gibi çeşitli değerler…

Sadece belli zaman geçerli olacak linkler

Açıkçası bu tarz gereksinimler için farklı yöntemler ve yaklaşımlar tabi ki mümkün. Çok derinlere girmeden, .NET platformunda, bu tarz ihtiyaçları nasıl karşılayabiliriz kısaca bundan bahsetmeye çalışacağım.

.NET ve özellikle ASP.NET Core günümüz uygulamalarının güvenlik ihtiyaçlarını karşılayabilmek için birçok API ile bize yol gösteriyor bildiğiniz gibi. Güçlü şifreleme API’ları, HTTPS kavramları, CORS mekanizmaları, CRSF önlemleri, veri koruma, kimlik doğrulama, yetkilendirme, “secret” kullanımı gibi gibi…

Yukarıda az önce bahsetmiş olduğum gereksinim için, “veri koruma” çatısı altında, .NET’de ki ITimeLimitedDataProtector ara yüzüne bakalım. Sadece belli bir süre geçerli olacak veriler ya da ifadeleri bu arayüzün sağladığı metotlar ile karşılayabiliyoruz.

Bu arayüzün metotlarını kullanabilmek için, öncelikle Microsoft.AspNetCore.DataProtection.Extensions kütüphanesine ihtiyacımız var. Genel olarak bu kütüphane .NET içerisinde “veri koruma” özelliklerinin dışarı sunulduğu bir kütüphane.

ITimeLimitedDataProtector arayüzünü kullanabilmek için öncelikle bir tane “DataProtectionProvider” yaratmamız, daha sonra bu “provider” ile de verimizi koruyacak bir koruyucu tanımlamamız gerekmekte.

            var timeLimitedDataProtector = DataProtectionProvider.Create("SomeApplication")
                .CreateProtector("SomeApplication.TimeLimitedData")
                .ToTimeLimitedDataProtector();

Burada metotların parametrelerine baktığınızda göreceğiniz “string” ifadeler önemli; oluşturulan provider ve DataProtector‘lerin bir nevi etiketlenmesi olarak düşünülebilirsiniz. Bu etiketlemeye göre, verilerin güvenliğinin amacı ve kapsamı belirtilmiş oluyor gibi düşünebiliriz. Bu ifadeler veriyi korumak için kullanılacak anahtarların oluşturulmasında kullanılıyor. Böylece, DataProtectionProvider.Create(“abc”) ile oluşturduğunuz bir provider, DataProtectionProvider.Create(“xyz”) şeklinde oluşturulan provider’ın güvenliğini sağladığı ifadelere erişemiyor.

DataProtectionProvider.Create() metodunun parametrelerine baktığınızda veriyi korumak için bazı özellikleri ayarlayabileceğimizi görüyoruz. DirectoryInfo tipinde bir dizin ile veri korumak için kullanılacak anahtarların nerede olacağını ya da X509Certificate2 ile de anahtarların ek olarak bir bir sertifika ile şifreleneceğini belirtebiliyoruz. Bunların çok ayrıntısına girmeyeceğim ama burada belirtmek istediğim, parametreler ile veri koruma şekillerini özelleştirip, koruma yaklaşımlarını değiştirmek mümkün.

Bu şekilde yarattığımız timeLimitedDataProtector değişkeni üzerinden korumak istediğimiz ifadeyi, Protect() metodu ile zaman aralığı belirtip koruyama alıyoruz.

ProtectedData = timeLimitedDataProtector.Protect(plaintext: jsonString
                    , lifetime: TimeSpan.FromSeconds(LifeTime));

Yukarıdaki ifade ile “Hello World” ifadesini şifreleyip, hash’leyip koruma altına alıyoruz. ProtectedData özelliğimiz 20 saniye geçerli olmak üzere aşağıdakine benzer bir yapıya dönüşüyor.

VGhlIGFuc3dlciB0byB0aGlzIGlzIHZlcnkgc2ltcGxlLiBJdCB3YXMgYSBqb2tLiBJdCBoYWQgdG8gYmUgYSBudW1iZXIsIGFuIG9yZGluYXJ5LCBzbWFsbGlzaCBudW1iZXIsIGFuZCBJIGNob3NlIHRoYXQgb25lLiBCaW5hcnkgcmVwcmVzZW50YXRpb25zLCBiYXNlIHRoaXJ0ZWVuLCBUaWJldGFuIG1vbmtzIGFyZSBhbGwgY29tcGxldGUgbm9uc2Vuc2UuIEkgc2F0IGF0IG15IGRlc2ssIHN0YXJlZCBpbnRvIHRoZSBnYXJkZW4gYW5kIHRob3VnaHQgJzQyIHdpbGwgZG8nIEkgdHlwZWQgaXQgb3V0LiBFbmQgb2Ygc3Rvcnku

Protect() metodunun lifetime parametresi ile TimeSpan şeklinde her türlü zamanı belirtebiliyoruz tabi ki de.

Koruma altına aldığımız; şifrelenmiş ve hash’lenmiş ifadeyi, bu örnekteki gibi 20 saniye içerisinde Unprotect() metodu ile açıp, tekrardan “Hello World” ifadesine ulaşabiliyoruz. Ama 20 saniyeden sonra bu değere ulaşmak mümkün olamıyor ve koruduğumuz veri geçerliliğini kaybediyor.

string data = timeLimitedDataProtector.Unprotect(protectedData);

Bu API ile uzun süre ya da belirsiz süre boyunca veri korunması tavsiye edilmiyor. Bunun sebebi korumaya aldığınızda veriyi şifrelemek ve hash’lemek için kullanılan anahtarların sürekliliğinin sağlanmasının riski. Eğer uzun süre koruma altında saklamanız gereken ifadeler varsa, farklı yöntemler ile ilerlemek mümkün ya da bu API’ın sağladığı interface’ler ile kendi ihtiyaçlarımıza göre farklı geliştirmeler yapılabilir.

Önemli bir nokta da, koruma altına sadece “text” ifadeler alabiliyoruz. Dolayısıyla biraz daha kompleks verileri, “serialize” edip (Ör: JsonSerializer gibi) koruma altına almak mümkün.

Komple resmi daha net görmek için, örnek olması için bir tane ASP.NET Core uygulaması üzerinden Razor sayfa modelinin koduna bakalım.


namespace SomeApplication.Pages
{
    using Microsoft.AspNetCore.DataProtection;
    using Microsoft.AspNetCore.Mvc.RazorPages;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Text.Json;


    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        public string ProtectedData { get; private set; }
        public string Data { get; private set; }
        public int LifeTime { get; private set; } = 300;
        public string Error { get; private set; }


        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }

        public void OnGet(string protectedData)
        {
            var timeLimitedDataProtector = DataProtectionProvider.Create("SomeApplication")
                .CreateProtector("SomeApplication.TimeLimitedData")
                .ToTimeLimitedDataProtector();

            //URL'de ?protecdata= ifadesi boş
            if (string.IsNullOrEmpty(protectedData))
            {
                //Bir tane basit nesne modelimiz olsun
                var data = new SomeDataModel
                {
                    Name = "Arda Cetinkaya",
                    EMail = "somemail@mail.com",
                    SomeDate = DateTimeOffset.Now
                };

                //Bu modelimizi JSON olarak "text" şeklinde ifade edelim.
                string jsonString = JsonSerializer.Serialize(data, new JsonSerializerOptions
                {
                    WriteIndented = true
                });

                Data = jsonString;

                //Yukarıda LifeTime=20sn. şeklinde tanımladığımız özellikle, modelimizi
                //20 saniye geçerli olabilecek bir ifade ile korumaya alalım.
                ProtectedData = timeLimitedDataProtector.Protect(plaintext: jsonString
                    , lifetime: TimeSpan.FromSeconds(LifeTime));
            }
            else
            {
                //Sayfaya ?protecdata=a412Fe12dada... şeklinde erişim olduğunda
                try
                {
                    //Gelen değerin korumasını kaldırıp, korumaya aldığımız değere ulaşalım
                    string data = timeLimitedDataProtector.Unprotect(protectedData);
                    Data = "Data is valid";
                }
                catch (Exception ex)
                {
                    Error = ex.Message;

                }

            }
        }
    }

    public class SomeDataModel
    {
        public string Name { get; set; }
        public string EMail { get; set; }
        public DateTimeOffset SomeDate { get; set; }
    }
}

Yukarıdaki örnekte, bir JSON ifadeyi 20 saniye geçerli olacak bir korumaya alıyoruz ve bunu bir link ile ilişkilendiriyoruz. 20 saniye boyunca link geçerli olacak ve korumaya aldığımız değer geçerli olacaktır. Ama 20 saniye sonunda korumaya aldığımız veri geçerliliğini kaybedecektir.

Bu basit ve hızlı yazı ile uzun bir aradan sonra birşeyler yazmış olmak bana fayda getireceği gibi umarım bu yazı da bazı soru işaretlerini gidermek için sizlere bir kapı açar ve çeşitli çözümlerinizde fayda sağlar. Bir sonraki yazıda görüşmek üzere.

Yazıda küçük bir anektot saklı, bulan olur ve yüzünde küçük bir gülümseme oluşursa ne mutlu 😊