ASP.NET Core’da ile Farklı Configuration Provider Geliştirme

Okuma Süresi: 3 dakika

ASP.NET Core ile oluşturmuş olduğunuz bir uygulamaya ait belli başlı ayarları olabilir. ASP.NET Core’da varsayılan olarak File Configuration Provider (INI, JSON ve XML dosyaları) kullanılmaktadır. Ancak bazı durumlarda ayarlarımızı farklı ortamlarda saklayıp yüklememiz gerekebilir. Örneğin Redis’te, Vault’ta veya environment variable’da (ortam değişkenleri) saklayabiliriz. Bu gibi durumlarda kendi configuration provider’ımızı geliştirmemiz gerekmektedir.

Bugün yapacağımız örnekte uygulama ayarlarını Redis’te tutup uygulama başlatılırken ayarların okunmasını sağlayacağız.

Öncelikle aşağıdaki komutla yeni bir ASP.NET Core uygulaması oluşturup metin editörümüzle veya IDE’mizle açalım.

dotnet new mvc -n AspNetCoreCustomConfigSample -o aspnetcore-custom-config-sample

Uygulamamız üzerinden Redis’e bağlanabilmek için StackExchange.Redis paketini kullanacağız. Aşağıdaki komutu yazarak ilgili paketi projemize referans olarak ekliyoruz.

dotnet add package StackExchange.Redis

Ayarlarımızı Redis üzerinde Settings ismindeki anahtarda tutacağız. İçeriği aşağıdaki gibi JSON yapıda olacaktır.

{
  "AppName": "Redis Configuration Sample",
  "AppVersion": "1.0.0"
}

Geliştireceğimiz sınıflar aşağıdaki gibi olacaktır:

  • Configuration Provider
  • Configuration Source
  • Extension Methods

İlk önce Configuration Provider sınıfımızı geliştirmeye başlayacağız. RedisConfigurationProvider isminde bir sınıf oluşturalım ve içeriğini aşağıdaki gibi değiştirelim.

using aspnetcore_custom_config_sample.Model;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using StackExchange.Redis;

namespace aspnetcore_custom_config_sample.Configuration
{
	public class RedisConfigurationProvider : ConfigurationProvider
	{
		private readonly string _redisHost;

		public RedisConfigurationProvider(string redisHost)
		{
			_redisHost = redisHost;
		}

		public override void Load()
		{
			var options = ConfigurationOptions.Parse(_redisHost);
			var connectionMultiplexer = ConnectionMultiplexer.Connect(options);
			var redisSettings = connectionMultiplexer.GetDatabase().StringGet("Settings");
			var settings = JsonConvert.DeserializeObject<AppSettings>(redisSettings);

			Data.Add("AppName", settings.AppName);
			Data.Add("AppVersion", settings.AppVersion);
		}
	}
}

Yazmış olduğumuz Configuration Provider sınıfında base sınıftaki Load metodunu ezmekteyiz. Load metodunun içerisinde Redis’e bağlanarak ilgili ayarları okuyoruz ve yine base sınıftan gelen Data ismindeki Dictionary‘nin içeriğini dolduruyoruz.

İkinci adımda ise Configuration Source sınıfımızı geliştireceğiz. RedisConfigurationSource isminde bir sınıf ekliyoruz ve içeriğini aşağıdaki şekilde değiştiriyoruz.

using Microsoft.Extensions.Configuration;

namespace aspnetcore_custom_config_sample.Configuration
{
	public class RedisConfigurationSource : IConfigurationSource
	{
		private readonly string _redisHost;

		public RedisConfigurationSource(string redisHost)
		{
			_redisHost = redisHost;
		}

		public IConfigurationProvider Build(IConfigurationBuilder builder)
		{
			return new RedisConfigurationProvider(_redisHost);
		}
	}
}

Configuration Source sınıfımız IConfigurationSource sınıfından türemektedir. IConfigurationSource interface’i Build isminde bir metot bulundurmaktadır. Bu sınıf bir configuration provider’ı temsil etmek için bulunmaktadır. Bizim Configuration Source sınıfımız ise RedisConfigurationProvider’ı temsil etmektedir. Constructor metottan parametre olarak Redis sunucumuzun adresini almaktadır.

Şimdi sıra extension metotları geliştirmeye geldi. ConfigurationExtensions isminde bir sınıf oluşturarak içeriğini aşağıdaki şekilde değiştirelim.

using Microsoft.Extensions.Configuration;

namespace aspnetcore_custom_config_sample.Configuration
{
	public static class ConfigurationExtensions
	{
		public static IConfigurationBuilder AddRedisConfiguration(this IConfigurationBuilder configuration, string redisHost)
		{
			configuration.Add(new RedisConfigurationSource(redisHost));
			return configuration;
		}
	}
}

Bu sınıfımızın amacı ise IConfigurationBuilder tipindeki bir nesne örneğine Configuration Source sınıfımızın nesne örneğini eklemektedir. Bu sayede ASP.NET Core uygulamamız artık Redis’ten uygulama ayarlarımızı okuyabilecek hale gelecektir.

Son adım olarak extension metodumuzu kullanmak kaldı. Bunun için Program.cs dosyamızı aşağıdaki şekilde değiştiriyoruz.

using aspnetcore_custom_config_sample.Configuration;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;

namespace AspNetCoreCustomConfigSample
{
	public class Program
	{
		public static void Main(string[] args)
		{
			BuildWebHost(args).Run();
		}

		public static IWebHost BuildWebHost(string[] args) =>
			WebHost.CreateDefaultBuilder(args)
				.ConfigureAppConfiguration(AddRedisConfiguration)
				.UseUrls("http://localhost:5000")
				.UseStartup<Startup>()
				.Build();

		private static void AddRedisConfiguration(WebHostBuilderContext context, IConfigurationBuilder builder)
		{
			var configuration = builder.Build();
			builder.AddRedisConfiguration("localhost");
		}
	}
}

ConfigureAppConfiguration metodu parametre olarak bir delegate almaktadır. Delegate ettiğimiz metot ile AddRedisConfiguration extension metodunu kullanarak ASP.NET Core’un uygulamamız yüklenirken ayarlarımızı Redis’ten okumasını sağladık.

Şimdi ayarlarımızı okumaya geldi. Ayarlarımızı örnek olması açısından HomeController üzerinden okuyacağız. HomeController’ın içeriğini aşağıdaki şekilde değiştiriyoruz.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using AspNetCoreCustomConfigSample.Models;
using aspnetcore_custom_config_sample.Model;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Configuration;

namespace AspNetCoreCustomConfigSample.Controllers
{
	public class HomeController : Controller
	{
		private readonly IConfiguration _appSettings;

		public HomeController(IConfiguration config)
		{
			_appSettings = config;
		}

		public IActionResult Index()
		{
			ViewBag.AppName = _appSettings.GetSection("AppName").Value;
			ViewBag.AppVersion = _appSettings.GetSection("AppVersion").Value;

			return View();
		}
	}
}

Dependency Injection yardımıyla IConfiguration tipinde bir nesne örneği alıyoruz. Index action’ında ise AppName ve AppVersion değerlerini okuyarak ViewBag‘lere atıyoruz. AppName ve AppVersion isimleri ise Configuration Provider sınıfımızda Data Dictionary‘sine eklemiş olduğumuz anahtar alanlardır.

Son olarak Home/Index.cshtml dosyamızı aşağıdaki şekilde düzenliyoruz.

@{
    ViewData["Title"] = "Home Page";
}

<h1>@ViewBag.AppName</h1>
<h2>@ViewBag.AppVersion</h2>

Uygulamızı tarayıcımızla ile açtığımızda aşağıdaki ekran görüntüsünü görmekteyiz.

Örnek proje dosyalarına https://github.com/mennan/aspnetcore-custom-config-sample adresinden ulaşabilirsiniz.

Docker Image’larının HTTP API Kullanılarak Silinmesi

Okuma Süresi: 1 dakika

Merhabalar.

Bu yazımda private container registry’de bulunan Docker image’larının Docker’ın HTTP API’ını kullanarak nasıl silineceğinden bahsedeceğim.

Image’ların silinmesi 2 adımdan oluşmaktadır. İlk adım image ismi ve tag değerinin digest değerini almak, ikinci adım ise digest değerini API’a göndererek Docker üzerinden image’ın silinmesi ile sağlamak.

Digest değerini almak için öncelikle container registry’e aşağıdaki gibi bir HTTP isteği göndermek gerekmektedir.

GET /v2/exampleimage/manifests/latest HTTP/1.1
Host: cr.example.com
Accept: application/vnd.docker.distribution.manifest.v2+json
Authorization: Basic YXNkOmFzZA==
User-Agent: PostmanRuntime/7.15.0
Cache-Control: no-cache

Container registry’e bağlanmak için Basic Authentication kullanılmaktadır. Authorization header’ında ilgili kullanıcının adı ve şifre değeri gönderilmelidir. HTTP isteğinin path bölümünde bulunan exampleimage ise silinmek istenen image’ın ismidir. Path’in son kısmında yazan latest ise silinmek istenen image’ın sürümüdür.

İsteği gönderdikten sonra aşağıdakine benzer bir cevap gelmelidir.

{
    "schemaVersion": 2,
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "config": {
        "mediaType": "application/vnd.docker.container.image.v1+json",
        "size": 4617,
        "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
    },
    "layers": [
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 22496034,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 17694102,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 2977123,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 62093437,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 136,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 18044387,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        }
    ]
}

Digest değerimiz gelen cevaptaki 7. satırda bulunmaktadır.

Digest değerimizi aldığımıza göre artık image’ı silme işlemine geçebiliriz. Silme işlemini gerçekleştirmek için aşağıdaki gibi bir HTTP isteği göndermemiz gerekmektedir.

DELETE /v2/exampleimage/manifests/sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 HTTP/1.1
Host: cr.example.com
Accept: application/vnd.docker.distribution.manifest.v2+json
Authorization: Basic YXNkOmFzZA==
User-Agent: PostmanRuntime/7.15.0
Cache-Control: no-cache

Yukarıdaki örnek HTTP isteğinde görülebileceği gibi http://cr.example.com//v2/exampleimage/manifests/sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 adresine DELETE isteği atmamız yeterlidir. Adresimizin sonunda sha256 ile başlayan ifade, bizim image’ımızın digest değeridir.

İsteği gönderdikten sonra herhangi bir cevap gelmeyecektir. İşlemin başarılı olup olmadığını gelen HTTP durum kodundan anlayabiliriz. Gelen durum kodu HTTP 202 Accepted ise image silme işlemi başarıyla gerçekleşmiş demektedir.

Bu işlemleri otomatik yapan .Net Core projesine https://github.com/mennan/DockerImageRemover adresinden ulaşabilirsiniz.

.Net Core’da HttpClient ile Proxy Kullanımı

Okuma Süresi: 2 dakika

.Net Core ile geliştirdiğimiz uygulamalarda HttpClient sınıfını kullanarak uç noktalara istek atmamız gerekebilir. Örnek olarak kurumsal firmalarda çalışacak olan uygulamanız internet ortamındaki bir adrese istek göndermek isteyebilir. Ancak uygulamanızın çalışmış olduğu sunucunun internet erişimi kısıtlandığından dolayı ilgili adrese erişemeyebilirsiniz veya HttpClient sınıfı kullanarak göndermiş olduğunuz isteği ve gelen cevabı Charles Web Debugging Proxy gibi uygulamalar ile araya girerek takip etmek isteyebilirsiniz. Bu işlemleri yapabilmeniz için HttpClient ile birlikte proxy kullanmanız gerekmektedir.

HttpClient ile atılan isteklerde proxy tanımı yapabilmek için HttpClientHandler ve WebProxy sınıflarına ihtiyacımız vardır.

Örnek uygulama olarak .Net Core’da bir console projesi oluşturacağız. Aşağıda bash komutları ile projemizi oluşturalım.

dotnet new console -n NetCoreHttpClientProxy
cd NetCoreHttpClientProxy

Projemizi kullanmış olduğumuz metin editörü veya IDE ile açarak aşağıdaki şekilde kodlarımızı yazalım.

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace NetCoreHttpClientProxy
{
	class Program
	{
		static async Task Main(string[] args)
		{
			const string ProxyUrl = "http://localhost:5001";
			const string ProxyUsername = "";
			const string ProxyPassword = "";
			const string RequestUrl = "https://google.com";

			var proxy = new WebProxy
			{
				Address = new Uri(ProxyUrl),
				Credentials = new NetworkCredential(ProxyUsername, ProxyPassword)
			};

			var httpClientHandler = new HttpClientHandler
			{
				Proxy = proxy,
				UseProxy = true
			};

			using (var client = new HttpClient(httpClientHandler))
			{
				var response = await client.GetAsync(RequestUrl);

				if (response.IsSuccessStatusCode)
				{
					var responseString = await response.Content.ReadAsStringAsync();
					Console.WriteLine(responseString);
				}
			}
		}
	}
}

Yapmış olduğumuz işlemleri açıklamak gerekirse https://google.com adresine http://localhost:5001 proxy adresini kullanarak istek göndermekteyiz.

Öncelikle WebProxy sınıfını kullanarak proxy adresini ve proxy’ye ait kullanıcı adı ve şifre tanımlamalarını yapıyoruz. Eğer proxy’ye ait herhangi bir kullanıcı adı ve şifre bilgisi bulunmuyorsa Credentials özelliğine değer vermenize gerek yoktur.

Sonrasında ise oluşturmuş olduğumuz WebProxy nesnesini HttpClientHandler tipindeki bir nesnenin Proxy özelliğine atıyoruz ve ilgili nesne örneğinin UseProxy özelliğini true olarak ayarlıyoruz.

En son işlem olarak ise HttpClient tipinin nesne örneğini oluştururken yapıcı metoda (constructor) HttpClientHandler tipindeki nesne örneğini vermek. Bu aşamadan sonra HttpClient tipindeki nesne örneğinden gönderilecek işlem proxy üzerinden geçerek atılacaktır.

Örnek kodlara https://github.com/mennan/netcore-httpclient-proxy adresinden ulaşabilirsiniz.