Dotnet Core HttpClient Kullanımı

29 Eki

Dotnet core veya .Net Framework uygulamalarında bir Web servisine erişmek için HttpClient sınıfından bir nesne oluşturulur ve kullanılır. Buraya kadar her şey normal. İşlem bittikten sonra kullanılan bu nesne yok edilir. İşte bu durumda işler biraz karışmaya başlar.

HttiClinet using blok ile kullanımı

public class ToDoClient : IToDoClient
{
	public async Task<Todo> Get(int id)
	{
		using(var client = new HttpClient())
		{
			BaseAddress = new Uri(
				"https://jsonplaceholder.typicode.com");
		}
		
		return await client.GetFromAsync<ToDo>(
			$"/todos/{id}");
	}
}

HttpClient sınıfı, IDisposible interface uyguladığı için, using bloğu ile kullanılabilir. Using bloğunda oluşturulan nesneler, blok sonunda ortadan kaldırılır. Ancak nesnenin kullandığı soket hemen serbest bırakılmaz. Ağır yük altında kullanılabilir soket sayısı azalır hatta tükenebilir. Burada konu ile ilgili bir yazı bulunmaktadır.

Yeniden kullanılabilir HttpClient nesnesi

Bu durumun önüne geçmek için tek bir kez singleton olarak oluşturulan ve paylaşılan bir HttpClient nesnesi kullanılabilir.

public class ToDoClient : IToDoClient
{
   private readonly HttpClient client = new ()
	{
		 BaseAddress = new Uri(
			"https://jsonplaceholder.typicode.com");
	};

	public async Task<Todo> Get(int id)
	{		
		return await client.GetFromAsync<ToDo>(
			$"/todos/{id}");
	}
}

Bu çözüm ise, az sayıda kullanılan kısa ömürlü console uygulamaları için uygun olabilir. Ancak geliştiricilerin karşılaştığı diğer bir sorun, uzun süren işlemlerde paylaşılan bir HttpClient örneği kullanırken ortaya çıkar. HttpClient’in tekil veya statik bir nesne olarak başlatıldığı bir durumda, DNS ip değişiklikleri yapılırsa uygulama hata alır. Örneğin load balancer ile bir DNS birden fazla ip de bulunan sunuculara sırayla yönlendiriliyor olabilir.

builder.Services.AddSingleton<IToDoClient, ToDoClient>();

Bu arada ToDoClient Program.cs içerisinde singleton olarak tanımlanabilir. Eğer Transient veya Scoped olarak kullanılırsa her request sırasında yeniden nesne oluşturacaktır. Bu durumun önüne geçmek için HttpCleint nesnesi static olarak tanımlanabilir.

public class ToDoClient : IToDoClient
{
    private static readonly HttpClient client = new ()
	{
		 BaseAddress = new Uri(
			"https://jsonplaceholder.typicode.com");
	};

	public async Task<Todo> Get(int id)
	{		
		return await client.GetFromAsync<ToDo>(
			$"/todos/{id}");
	}
}

Ancak bu durumda BaseAddress ile tanımlanan DNS TTL süresi bittiğinde domain adı yeni bir ip adresini işaret eder. Ancak kod restart olmadan yeni ip adresini bilemez. Çünkü default HttpClient içerisinde bunu yakalayan bir mekanizma bulunmaz. Bu durumun yakalayabilmek için SockeHttpHandler kullanılabilir.

public class ToDoClient : IToDoClient
{
	private SocketsHttpHandler socketHandler = new()
	{
		PooledConnectionLifetime = TimeSpan.FromMinutes(5)
	};
	
    private static readonly HttpClient client = new (socketHandler)
	{
		 BaseAddress = new Uri(
			"https://jsonplaceholder.typicode.com");
	};

	public async Task<Todo> Get(int id)
	{		
		return await client.GetFromAsync<ToDo>(
			$"/todos/{id}");
	}
}

Ancak bu yöntem de en iyi çözüm yolu değildir. En iyi çözüm yolu  .NET Core 2.1 ile birlikte gelen IHttpClientFactory interface kullanmaktır.

IHttpClientFactory Kullanımı

Bu interface kullanmadan önce Program.cs içerisinde bir tanımlama yapılması gerekmektedir.

builder.Services.AddSingleton<IToDoClient, ToDoClient>();
builder.Services.AddHttpClient<IToDoClient, ToDoClient>(client =>
{
   client.BaseAddress = new Uri(
			"https://jsonplaceholder.typicode.com");
});

Bu düzenleme ile artık HttpClient aşağıdaki gibi kullanılabilir.

public class ToDoClient : IToDoClient
{
    private readonly HttpClient _httpClient;
	
	public ToDoClient(HttpClient httpClient)
	{
		_httpClient = httpClient;
	}
	
	public async Task<Todo> Get(int id)
	{		
		return await _httpClient.GetFromAsync<ToDo>(
			$"/todos/{id}");
	}
}

Bu durumda HttpClient nesnesi IHttpClientFactory tarafından yönetilmektedir. Çünkü ortak bir havuzdaki HttpMessageHandler nesneleri, birden çok HttpClient örneği tarafından yeniden kullanılabilen nesnelerdir. Bu sayede DNS sorunu çözülmektedir.

Named Client kullanımı

Servis binding sırasında AddHttpClient() metoduna bir isim vererk kullanmak mümkündür.

builder.Services.AddSingleton<IToDoClient, ToDoClient>();
builder.Services.AddHttpClient("TodoApi",client =>
{
   client.BaseAddress = new Uri(
			"https://jsonplaceholder.typicode.com");
});
public class ToDoClient : IToDoClient
{
    private readonly IHttpClientFactory _httpClientFactory;
	
	public ToDoClient(IHttpClientFactory httpClientFactory)
	{
		_httpClientFactory = httpClientFactory;
	}
	
	public async Task<Todo> Get(int id)
	{		
	    var client = _httpClientFactory.CreateClient("ToDoApi");
		
		return await client.GetFromAsync<ToDo>(
			$"/todos/{id}");
	}
}

Bu sayede doğrudan IHttpClientFactory interface nesneleri ile HttpClient request’leri en verimli şekilde gerçekleştirilebilmektedir.

Kaynaklar

  • https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
  • https://www.aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

Serilog .net core 6 Kullanımı

25 Eyl

Microsoft .net core 6 bazı yenilikleri de beraberinde getirdi. Yeni bir proje oluşturulduğunda karşımıza çıkan ilk yenilik minimal asp.net uygulaması olacaktır. Asp.net core 6 ile Program ve Starup class sınıfları kaldırılarak minimal api dünyasına giriş yapılmıştır.

  • Minimal şablonda Program ve Startup class’lar bulunmaz. Program.cs içerisinde class bulunmayan düz bir yapı bulunur.
  • Konfigurasyonun oluşturulabilmesi için yeni bir WebApplicationBuilder API bulunu.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.Run();

Uygulamaya ait bütün ayarlar bu dosya içerisinde yapılandırıldığından Serilog ayarları da burada konumlandırılır. (.Net Core 6 konfigürasyon ayarları.)

Neden Serilog Kullanılır?

Bir .net 6 uygulamasında üretilen log’ların bir çok farklı yere akması (sink) istenebilir. Örneğin konsol, metin dosyası, SqlServer, Oracle, Elasticsearch, Kafka, Redis, vs. gibi. Serilog altyapısı, geliştiricilere sadece konfigürasyon ayarları ile, kodlama yapısına hiç dokunmadan, log bilgilerini farklı kaynaklara aktarabilme imkanı sağlar.

Uygulamaya Serilog eklenebilmesi için Serilog.AspNetCore paketinin eklenmesi gerekir.

dotnet add package Serilog.AspNetCore

veya

NuGet\Install-Package Serilog.AspNetCor

Örneğin Apache Kafka’ya log yazabilmek için Serilog.Sinks.Kafka nuget paketinin uygulamaya eklenmesi gerekir.

.Net Core 6 da Serilog Kullanımı

Bir önceki .Net 5 versiyonunda serilog implementasyonu aşağıdaki gibidir.

using Serilog;

var builder = WebApplication.CreateBuilder(args)
    .UseSerilog(...);

// Error CS1929 : 'WebApplicationBuilder' does not contain a definition for 'UseSerilog' and
// the best extension method overload 'SerilogWebHostBuilderExtensions.UseSerilog(
// IWebHostBuilder, ILogger, bool, LoggerProviderCollection)' requires a receiver of type
// 'IWebHostBuilder'

Bu uygulanış şekli .net 6 versiyonunda değişmiştir. Bu nedenle yukarıdaki hata mesajı alınacaktır. Doğru uygulama şekli aşağıdaki gibidir.

IHostBuilder içerisinde bulunan builder.Host kullanılır.

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((builderContext, loggerConfiguration) => loggerConfiguration
    .WriteTo.Console()
    .WriteTo.Kafka());

var app = builder.Build();

Bu şekilde yapılandırma ile hem konsol hem de Kafka’ya log akması sağlanabilir. Bu şekilde düzenlenen Serilog yapılandırmasında, ayarlar kod tarafında verilir. Örneğin Kafka sunu ayarları değiştiğinde aşağıdaki gibi düzenleme yapılır.

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((builderContext, loggerConfiguration) => loggerConfiguration
    .WriteTo.Console()
    .WriteTo.Kafka("MyTopicName", "172.15.45.3:9092"));

var app = builder.Build();

Konfigürasyonun appsettings.json üzerinde yapılması

Serilog ayarları appsettings.json dosyası içerisinde Serilog section içerisinde yapılandırıldığında, kod tarafında bir değişiklik yapmaya gerek kalmaz.

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Information",
        "System": "Information"
      }
    },
    "WriteTo": [
      {
        "Name": "Kafka",
        "Args": {
          "batchSizeLimit": "50",
          "period": "5",
          "bootstrapServers": "localhost:9092",
          "topic": "logs"
        }
      }
    ]
  }
}

Bu konfigurasyonun okunabilmesi için kod tarafında bir değişiklik gerekir.

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((builderContext, loggerConfiguration) => loggerConfiguration
    .ReadFrom.Configuration(builderContext.Configuration));

var app = builder.Build();

Burada dikkat edilmesi gereken noktalardan biri, eğer MinimumLevel bölümündeki Override bölümünde bulunan Microsoft veya System kategorileri aşağıdaki gibi Warning veya Error gibi seviyelere çekilirse, geliştirme aşamasında üretilen Information logları akmaz. Bu durumda uygulamanın çalışmadığı hissine kapılmamak gerekir.

    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },

Bu ayarlamaya göre, MinimumLevel olarak Debug seviyesinde oluşan log bilgileri akar. Override kısmında hangi kategoriye göre hangi seviyede log akışı olacağı bilgileri yer almaktadır. Bilindiği gibi .net core ile sınıflar içerisinde log bilgilerini yazmak için ILogget<TCategoryName> interface kullanılır. Örneğin:

 public CategoryController(ILogger<CategoryController> logger)
 {
    logger.LogInformation("CategoryController started.");
    logger.LogError("Error!");
 }

TCategryName log kullanan ilgili sınıfın (CategoryController) ismidir. Fakat log işlemlerinde arka planda bu sınıfın tam ismi kullanılır. Örneğin Shopping.WebApi.Controllers.CategoryController şeklinde tam isim alınır. Nemespace bilgisi Shopping ile başlayan kategori loglarını information seviyesinde akıtmak için Override bölümü aşağıdaki şeklilde düzenlenebilir.

  "MinimumLevel": {
     "Default": "Debug",
     "Override": {
       "Microsoft": "Warning",
       "System": "Warning",
       "Shopping": "Information"
    }
  },

Extension method yöntemi

Extension method yöntemi kullanılarak Program.cs dosyası içeriği sadeleştirilebilir.

public static class LoggingExtensions
{
   public static void AddSerilogConfiguration(this IHostBuilder builder)
   {
       Action<HostBuilderContext, IServiceProvider, LoggerConfiguration> ConfigureLogger =
                (builderContext, serviceProvider, loggerConfiguration) =>
                            loggerConfiguration.ReadFrom.Configuration(builderContext.Configuration)
                                       .Enrich.FromLogContext();
            
       builder.UseSerilog(ConfigureLogger);
   }
}
var builder = WebApplication.CreateBuilder(args);

builder.Host.AddSerilogConfiguration();

var app = builder.Build();

İkinci Alternatif olarak Serilog uygulamasında aşağıdaki yöntem kullanılabilir.

public static class LoggingExtensions
{
    public static void AddSerilogConfiguration(this ILoggingBuilder loggingBuilder, IConfiguration configuration)
    {
        var logger = new LoggerConfiguration()
            .ReadFrom.Configuration(configuration)
            .Enrich.FromLogContext()
            .CreateLogger();

        Log.Logger = logger;

        loggingBuilder.ClearProviders();
        loggingBuilder.AddSerilog(logger);
    }
}

Not: Program.cs içerisinde uygulama başlatma bilgileri için Serilog namespace altındaki Log static class kullanılabilir.

var builder = WebApplication.CreateBuilder(args);

Log.Information("test"); // builder.Build() olmadan bu çalışmaz.

builder.Host.AddSerilogConfiguration();

var app = builder.Build();

try 
{
     Log.Information("Application starting.";
     app.Run();
} 
catch(Exception exception) 
{
    Log.Fatal("Application terminated unexpectedly.");
} 
finally 
{
    Log.CloseAndFlush();
}

Dotnet Core Configuration Ayarları

1 Eyl

ASP.NET Core’da (6.0 versiyonda) bir uygulamaya dışarıdan bir konfigürasyon ayarı, bir veya daha fazla configuration provider kullanılarak gerçekleştirilebilir. Provider diye adlandırılan araçlar, appsettings.json dosyaları veya environment variables gibi çeşitli yapılandırma kaynaklarını kullanarak konfigürasyon ayar verilerini okurlar.

Uygulamada Configuration ayarları, Program.cs dosyası içerisinde yapılabilir. Ayarların değerleri ise appsettings.json dosyası içerisinde bulunur. Örneğin aşağıdaki appsettings.json içerisinde Connection isimli bir section oluşturulmuştur.

{
  "Connection": {
    "Host": "appserver.com",
    "Port": "1524"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Connection ayarlarını uygulama içerisinde kullanmanın birden fazla yolu vardır. Bunlar aşağıda başlıklar halinde belirtilmiştir.

IConfiguration interface ile erişim

Bir class içerisinden Connection section ayarlarına erişmek için IConfiguration interface kullanılabilir. Bu interface, Microsoft.Extensions.Configuration içerisinde bulunur ve dependency injection ile istenen class içerisinden erişilebilir.

public class DatabaseService
{
  private readonly string _host;
  private readonly int _port;

  public DatabaseService(IConfiguration config)
  {
    _redisHost = config["Connection:Host"];
    _redisPort = Convert.ToInt32(config["Connection:Port"]);
  }
}

Ancak bu yöntem ile her Configuration ihtiyacı sırasında, class içerisinde section isimleri, değer isimleri ve casting işlemleri yapılmak zorundadır.

İşi bir adım daha ileriye taşıyabilmek adına, ilgili configuration section, bir nesneye dönüştürülebilir.

ConnectionOptions class:

public class ConnectionOptions
{
    public const string SectionName = "Connection";

    public string Host{ get; set; } = String.Empty;
    public string Port { get; set; } = String.Empty;
}

Options pattern ile erişim

Configuration için bir nesne tanımlandıktan sonra, ilgili class içerisinde kullanım şekli aşağıdaki gibi değiştirilebilir.

public class DatabaseService
{
  private readonly ConnectionOptions options;

  public DatabaseService(IConfiguration config)
  {
     options = new ConnectionOptions();
     config.GetSection(ConnectionOptions.SectionName)
           .Bind(options);
  }
}

Alternatif bir yol olarak ise aşağıdaki kullanım da tercih edilebilir.

public class DatabaseService
{
  private readonly ConnectionOptions options;

  public DatabaseService(IConfiguration config)
  {
     options = config.GetSection(ConnectionOptions.SectionName)
                  .Get<ConnectionOptions>();
  }
}

Options pattern kullanırken en yaygın yöntem, servis binding işlemlerinin kullanıcı class içerisinde değil de aşağıdaki gibi Program.cs içerisinde yapılmasıdır.

Options pattern IOptions<T> interface ile erişim

Configuration binding ayarlarını, tek bir extension method içerisine alarak Program.cs içeriği sadeleştirilebilir. Bu amaçla, aşağıda bir extension method tanımlanmıştır.

    public static class ConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, 
             IConfiguration config)
        {
             services.Configure<ConnectionOptions>(
                 config.GetSection(
                     ConnectionOptions.SectionName));

            return services;
        }
    }

Program.cs aşağıdaki gibi düzenlenebilir.

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddConfig(builder.Configuration);

var app = builder.Build();

Bu durumda DatabaseService class içeriği aşağıdaki şekilde değiştirilebilir.

public class DatabaseService
{
  private readonly ConnectionOptions options;

  public DatabaseService(IOptions<ConnectionOptions> options)
  {
     options = options.Value;
  }
}

Configuration section class’a dependency injection ile erişim

Bir Microsoft extension tipi olan IOptions<T> tipini kullanmadan, DatabaseService içerisine doğrudan ConnectionOptions tipi dependency injection yöntemi ile verilebilir. Bunun için öncelikle ConfigServiceCollectionExtensions sınıfı içerisinde küçük bir değişiklik yapılmalıdır. İki farklı alternatif ile singleton olarak ConnectionOptions tipine binding sağlayabiliriz.

ConfigServiceCollectionExtensions Seçenek-1

    public static class ConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, 
             IConfiguration config)
        {
             services.Configure<ConnectionOptions>(
                 config.GetSection(
                     ConnectionOptions.SectionName));
    
             services.AddSingleton(options => 
                 options => 
                 GetService<IOptions<ConnectionOptions>>().Value);

            return services;
        }
    }

ConfigServiceCollectionExtensions Seçenek-2

    public static class ConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, 
             IConfiguration config)
        {
             ConnectionOptions options = new();

             config.GetSection(ConnectionOptions.SectionName)
                     .Bind(options);
    
             services.AddSingleton(options);

            return services;
        }
    }

Bu durumda, DatabaseService içeriği aşağıdaki gibi yeniden düzenlenebilir. (Program.cs de hiç bir değişikliğe gerek yoktur, yukarıdaki ayarlar geçerlidir.)

public class DatabaseService
{
  private readonly ConnectionOptions options;

  public DatabaseService(ConnectionOptions options)
  {
     options = options;
  }
}

Artık servis sınıfı içerisinde Microsoft options pattern tipleri yerine, kendi sınıflarımızı kullanabilir hale getirmiş olduk.

Visual Studio Dotnet Core Web Api Proje Şablonu B3.Web.Template.V1

20 May

Bir aspnet core web api projesinde ihtiyaç duyulabilecek katmanları ve araçları bir araya getirerek B3.Web.Template.V1 bir proje şablonu hazırlayarak Github üzerinden yayınladım. Şablonu kullanabilmek için, projeyi Github üzerinden indirebilirsiniz.

Proje Katmanları

  • WebApi layer (B3.WebApi)
  • Application services layer (B3.Applcation)
  • Data access layer (B3.EntityFramework)
  • Domain layer (B3.Domain)
  • Infrastructure layer (B3.Infrastructure)

Katman ilişki şeması aşağıdaki gibidir.

Template Kullanımı

B3.Web.Template.V1 projesini indirin. Visual Studio 2019 için kullanılacak ise indirilen B3WebTemplate.zip dosyasını ProjectTemplates dizinine  ekleyin. Örneğin kenid bilgisayarımdaki yol şu şekildedir: (C:\Users\User\Documents\Visual Studio 2019\Templates\ProjectTemplates)

Visual Studio’yu açın ve yeni proje ekleme adımını uygulayın.

Proje adını girin.

Artık yeni web api projeniz tüm katmanlarıyla hazır hale gelmiştir.

ASP.NET Core geliştiriciler için yol haritası

5 Oca

2019 yılında, bir grup geliştirici, GitHub‘da ASP.NET Core geliştiriciler için bir yol haritası ortaya koymuşlar. Kaynağa buradan ulaşabilirsiniz.

Bu yazıda, her geliştirici için faydalı olabileceğini düşündüğüm bu yol haritasını sizlerle paylaşmak istedim.

Yol Haritası Boyunca Bilinmesi Gerekenler

  1. Öncelikle bilinmesi gerekenler.
    • C#
    • Entity Framework
    • ASP.NET Core
    • SQL Fundamentals
  2. Bilinmesi gereken genel geliştirme becerileri.
    • GIT sistemi öğrenilmeli, GitHub üzerinde yeni bir repository oluşturulabilmelidir, kod diğer geliştiricilerle paylaşılmalıdır.
    • HTTP(S) protokolü bilinmeli, http request metodları (GET, POST, PUT, PATCH, DELETE, OPTIONS) bilinmelidir.
    • Google kullanmaktan ve araştırmaktan korkmayın.
    • Dotnet CLI öğrenmelisiniz.
    • Veri yapıları ve algoritmalar hakkında bir kaç kitap okumalısınız.
  3. Dependency Injection (DI – Bağımlılıkların dışarıdan alınması)
  4. Veritabanları
  5. Cache Mekanizmaları
  6. Log Mekanizmaları
  7. Web site geliştirme şablonları (Template Engines)
  8. Gerçek zamanlı iletişim araçları (Realtime Communication)
  9. Nesne eşleştirme araçları (Object Mapping)
  10. API istemcileri (Clients)
  11. Bilinmesi faydalı olabilecek bilgiler
  12. Testler
  13. Görev zamanlayıcılar (Task scheduling)
  14. Mikro servisler (MicroServices)
  15. SOLID prensipleri
  16. Tasarım Kalıpları (Design-Patterns)

Yol Haritası Görseli

Asp.Net 5 verisyon numaralandırması Asp.Net Core 1.0 olarak değişiyor

11 Şub

Microsoft, son yıllarda yaptığı değişiklikle platform bağımsız teknolojiler geliştirmeye başlamıştır. Artık Microsoft ürünlerinin sadece Windows işletim sistemlerinde değil Linux ve Mac OS gibi işletim sistemlerinde de çalışabilmesi amaçlanmaktadır. Bu demek oluyor ki Microsoft geliştirme altyapısında köklü bir değişiklik yapılmaktadır. Artık biz geliştiricilere açık kaynak bir Microsoft platformu sunulmaktadır.

Bilindiği Asp.Net Framework sürüm serisi 1.0, 2.0, … ve 4.5 şeklinde devam etmekteydi. Bu sürümler üzerinde geliştirilen Web uygulamalarının yayınlanması IIS(Internet Information Services) üzerinden yapılmaktadır. Asp.Net 4.6 ve 5 sonrası için platform bağımsız bir yapının planlanması ile yeni versiyonun major numarasını değiştirerek yola devam etmek anlamsız olacaktı. Bu sebepten dolayı sürümler yeniden adlandırıldı.

Yapılan Yeni İsimlendirmeler

  • Asp.Net 5 yerine Asp.Net Core 1.0
  • .Net Core 5 yerine .Net Core 1.0
  • Entity Framework 7 yerine Entity Framework Core 1.0
  • .Net Framework yerine artık .NET Execution Environment (DNX) kullanılmaktadır.

image_60140ec0-ce46-4dbf-a14f-4210eab7f42c

Kaynaklar: