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.

Burada önemli olan diğer bir kısımda “namespace” üzerinde belirttiğimiz [assembly] özelliği. Burada, bu assembly’nin ya da *.dll’in içindeki belirttiğimiz sınıfının HostingStartup’ında kullanılabileceğini belirtiyoruz. Yani, bu “assembly”yi kullanan web uygulaması için burada belirtilen işlemler de geçerli olacak ve çalışacak. Şimdi de ASP.NET Core Web App(StartupDemos) projesi tarafına bakıp, bu oluşturduğumuz IHostingStartup ara yüzünün uygulanması nasıl olacak anlamaya çalışalım. Böyle diyip, aslında çok da bir şey yok demek pek olmayacak (😀) ama StartupDemos projesinde kod olarak da çok kritik bir kısım yok.

ASP.NET Core web projesinde önemli olacak kısım, Program.cs için de WebHost’u yaratırken ki kısım.

    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.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, 
                        "HostingStartupExternalLibrary");

                    webBuilder.UseStartup<Startup>();
                });
    }

Burada, WebHost başlarken, hangi “assembly”yi kullanacağımızı belirtiyoruz. Bunu webbuilder.UserSetting() ile yapabiliyoruz. ASP.NET Core SDK içinde gelen bazı WebHostDefaults enum tipinin sunduğu değerler ile belirtebiliyoruz. Burada IHostingStartup arayüzünü kullandığımız “assembly(*.dll)“nin adını yani HostingStartupExternalLibrary‘i veriyoruz.

webBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey,"HostingStartupExternalLibrary");

Birden fazla *.dll’i de noktalı virgün(;) ile ayırarak belirtmek mümkün.

webBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey,"DLL1;DLL2;DLL3");

Böyle belirtmek yerine “environment” değişkeniyle çalışma anı için kullanılacak şekilde; ASPNETCORE_HOSTINGSTARTUPASSEMBLIES değerinde belirtilebilir.

Yeri gelmişken küçük bir ip ucu da paylaşmak isterim. Geliştirme ortamında geliştirmelerimizi ve testlerimizi daha kolay yapabilmek için Visual Studio’da proje ayarlarından “environment” değişkenlerini ekleyebiliyoruz.

Web uygulaması ile ilişkisi nasıl?

Yazının başında dışarıdan bağımsız ayrı bir *.dll ile IHostingStartup uygulanmasını kullanabiliyoruz demiştim ama hala şu ana kadar ayrı bir *.dll şeklinde bir belirtecimiz olmadı. Dışarıdan bir *.dll ile kullanabilmek için ASP.NET Core Web projesine(StartupDemos) “assembly reference” şeklinde ekliyoruz.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="5.0.7" />
  </ItemGroup>
  <ItemGroup>
    <Reference Include="C:\\SomeCompany\\HostingStartupExternalLibrary.dll">
      <HintPath>C:\\SomeCompany\\HostingStartupExternalLibrary.dll</HintPath>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
  </ItemGroup>
</Project>

Burada hemen yeni bir soru akıllarda oluşmuş olabilir. “C:\SomeCompany” dizini de neyin nesi? Tamamen örnek olması adına uydurduğum bir dizin tabi ki. 😀Bir firmanın sağladığı ürün/araç geliştirme kütüphanesi için bir SDK kullandığımızı düşünelim. Bu SDK kullanımı için geliştirme ortamına bir kurulum yaptığımız ya da uygulamamızın çalışacağı ortama bazı bileşenler için kurulum yapmamız gerektiği ve uygulamamızın bu kurulumu referans olarak kullanması senaryolarını hatırlayalım.

Dolayısıyla HostingStartupExternalLibrary projesinin çıktısı “HostingStartupExternalLibrary.dll” dosyasını”C:\SomeCompany” dizini içerisine kopyalamamız gerekmekte.

IHostingStartup uygunlanmasını proje referansı olarak, Web projemiz ile bağımlı şekilde kullanmak da tabi ki mümkün,

Çok atla deve bir senaryomuz olmadığı için çok da komplike bir kodumuz yok ama yukarıda bir yerlerde hatırlarsanız, konfigürasyon olarak bazı değerleri eklemiştim. Şimdi o konfigürasyon değerleri üzerinden bütün bu yapının nasıl çalıştığına bakalım.

Web uygulamasında, Index.cshtml.cs içerisinde basitce aşağıdaki gibi konfigürasyon değerlerini alalım.

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

        public string Activity { get; private set; }
        public string Type { get; private set; }
        public string Key { get; private set; }

        public IndexModel(ILogger<IndexModel> logger, IConfiguration config)
        {
            _logger = logger;

            Activity = config["EXTERNAL_STARTUP_ACTIVITY"];
            Type = config["EXTERNAL_STARTUP_TYPE"];
            Key = config["EXTERNAL_STARTUP_KEY"];
        }
    }

Web uygulamamızın konfigürasyon dosyasında; appsettings.json, EXTERNAL_STARTUP_ACTIVITY, EXTERNAL_STARTUP_TYPE ve EXTERNAL_STARTUP_KEY gibi değerler yok. Bu değerler tamamen HostingStartupExternalLibrary.dll “assembly”sinden, IHostingStartup üzerinden web uygulamamıza aktarılmakta. “breakpoint” koyup web projesini çalıştırdığımız zaman konfigürasyon değerlerinin dolduğunu göreceğiz. Bu değerler tamamen HostingStartupExternalLibrary.dll ile ayarlanmış oluyor.

Kod akışının nasıl olduğunu anlamak için HostingStartupExternalLibrary projesinde IHostingStartup ara yüzünden yarattığımız sınıfın Configure() metodu içerisine de “breakpoint” koyup bakmanızı tavsiye ederim. Fark edeceksiniz ki, Startup.cs‘deki Configure() ve ConfigureServices() metotlarından önce IHostingStartup çalışacaktır.

Belki biraz hızlı bir yazı oldu ama umarım benzer tarz ihtiyaçlarınız için bir kapı açabilir. .NET ve ASP.NET Core‘un çalışma dinamiklerinin farkında olabilmek geliştirdiğimiz çözümlerin kalitesine de büyük katkılar sağlayabilir.

Bir sonraki yazıda görüşmek üzere. Mutlu kodlamalar 😉

*Yazıda geçen örnek tüm proje ve kodlara GitHub üzerinden ulaşabilirsiniz.