Web API keçidləri

Bütün Web Api -lərimiz Asp.Net Core 2.2 üzərində və arxa tərəfdə .Net Framework 4.6.2 üzərindən çalışırdı. Keçiddən sonra ilk qarşılaşdığımız problem Entity Framework Core ilə bağlı oldu. Beləki əvvəllər birbaşa EF üzərində SQL sorğusu çalışdırmaq üçün DbContext class-mızın içərisində DbQuery<T> təyin edib daha sonra onun vasitəsi ilə sorğumuzu icra edirdik. Məsələn

private async Task<List<long>> GetPermittedUserIds(int userId)
{
    return await _wpmDbContext.Query<OrganizationTreeUser>()
        .FromSql("select * from F_Hybrid_GetPermittedUsers(@userId)", new SqlParameter("@userId", userId))
        .Select(u => u.UserId).ToListAsync();
}

bu metodu çağıra bilməyimiz üçün DbContext class-na belə bir sətir əlavə edirdik

public virtual DbQuery<OrganizationTreeUser> OrganizationTreeUsers { get; set; }

Keçiddən sonra isə artıq hər şey DbSet<T> olaraq tanımlamalıyıq və əgət tipi yalnız sorğu nəticəsini map etmək üçün istifadə edəcəyiksə o zaman aşağıdakı dəyişiklikləri etməliyik

public virtual DbSet<OrganizationTreeUser> OrganizationTreeUsers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<OrganizationTreeUser>().HasNoKey();
    base.OnModelCreating(modelBuilder);
}
private async Task<List<long>> GetPermittedUserIds(int userId)
{
    return await _wpmDbContext.Set<OrganizationTreeUser>()
        .FromSqlInterpolated($"select * from F_Hybrid_GetPermittedUsers({userId})")
        .Select(u => u.UserId).ToListAsync();
}

Digər bir dəyişiklik isə yeni Asp.Net Core 3.0 -dan sonra gələn Endpoint Routing məsələsi ilə bağlı idi. Ona görə Startup.cs -də aşağıdakı əvəzləməni etdik

app.UseMvc();
//MVC proyetləri üçün
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});
//Web API proyetləri üçün
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

Başqa Asp.Net Core 2.2 üzərində olduğundan köhnə proyetlərimiz çox da xüsusi bir konfiqurasiyaya ehtiyac olmadı. Hər şey köhnə şəkildə olduğu kimi problemsiz işlədi. Amma əgər siz Asp.Net Web Forms və ya Asp.Net MVC 5 və altı tipli proyektlərdən keçid edirsinizsə struktur və konfiqurasiya olaraq daha çox şeyə ehtiyacınız ola bilər.

Worker Servislər - köhnə Windows servislər

Düzünü desəm ən çətin yerimiz burda oldu keçid zamanı. Çünki ümumi olaraq məntiq tam dəyişdi. Köhnə windows servis və yeni worker servis arasında fərq böyükdür. Worker servis ümumiyyətlə birbaşa olaraq windows servislərdən asılı deyil, əgər biz worker servisi windows servis kimi install etmək istəsək əlavə bir nuget paket yükləyirik və konfiqurasiya edirik. Məsələn Linux ortamda systemd vasitəsi ilə də worker servisləri istifadə etmək olar. Bu keçid zamanı tövsiyyəm belə olar dı ki, yeni worker servis proyekti yaradıb kodları ora köçürəsiniz. Birinci növbədə worker servisi windows servis kimi  install etmək üçün Microsoft.Extensions.Hosting.WindowsServices paketini yükləyib aşağıdakı hissəyə UseWindowsService() konfiqurasiyasını əlavə etməliyik.

     public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseWindowsService()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Service>();
            }).UseSerilog();

Uyğun işləri tamamladıqdan sonra test mərhələsində bəzi problemlər ortaya çıxdı.

Problem 1. Uzun çəkən start prosesini gözləməmə
Normalda bizim worker servislərdən biri qalxarkən əlavə bir servislə əlaqə qurub qeydiyyat edib sonra işlək vəziyyətə gəlirdi və bu proses 7-8 saniyə qalxırdı. Lakin net5.0-a keçiddən sonra bu prosesi gözləmədən servis işlək vəziyyətə keçirdi və 7-8 saniyə sonra  əgər xəta baş versə servis düşürdü. Ona görə necəsə bunu yoluna qoymalı idik. Bunun üçün UseWindowsService() hissəsini bir az araşdırma etdik və problemin kökünü tapdıq

public static IHostBuilder UseWindowsService(this IHostBuilder hostBuilder, Action<WindowsServiceLifetimeOptions> configure)
{
    if (WindowsServiceHelpers.IsWindowsService())
    {
        // Host.CreateDefaultBuilder uses CurrentDirectory for VS scenarios, but CurrentDirectory for services is c:\Windows\System32.
        hostBuilder.UseContentRoot(AppContext.BaseDirectory);
        hostBuilder.ConfigureLogging((hostingContext, logging) =>
        {
            logging.AddEventLog();
        })
        .ConfigureServices((hostContext, services) =>
        {
            services.AddSingleton<IHostLifetime, WindowsServiceLifetime>();
            services.Configure<EventLogSettings>(settings =>
            {
                if (string.IsNullOrEmpty(settings.SourceName))
                {
                    settings.SourceName = hostContext.HostingEnvironment.ApplicationName;
                }
            });
            services.Configure(configure);
        });
    }

    return hostBuilder;
}

Bu hissədə problem WindowsServiceLifetime - dan qaynaqlanırdı, çünki start üçün olan kod blokunu gözləmədən servis start vəziyyətə gəlirdi.

services.AddSingleton<IHostLifetime, WindowsServiceLifetime>();

Bunun üçün özümüzdə CustomWindowsServiceLifetime yaratdıq və əsl variantına 2 yerdə gözləmə üçün məntiq əlavə etdik. Əlsində nədənsə bu gözləmə məntiqi stop olunan yerdə var idi sadəcə start üçün yox idi və stop-da olan məntiqə uyğun dəyişiklik edib bu servisi startup zamanı register etdik. Sonda startup məntiqimiz bu formaya düşdü.

   public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .UseWindowsService()
                .ConfigureServices((hostContext, services) =>
                {
                    if (WindowsServiceHelpers.IsWindowsService())
                    {
                        //use this to obey service starts only of all starting tasks is successful
                        services.Replace(ServiceDescriptor.Singleton<IHostLifetime, CustomWindowsServiceLifetime>());
                    }

                    services.AddHostedService<Service>();
                }).UseSerilog();

CustomWindowsServiceLifetime class-nın içərisində əlavə etdiyimiz isə sadəcə bu dəyişikliklər oldu. Bununla da servisimiz artıq start olarkən lazım olan əməliyyatların başa çatmasını gözləyib sonra ya start olurdu ya da əgər xəta varsa düşürdü.

//yeni ManualResetEventSlim yaradırıq start əməliyyatı üçün
private readonly ManualResetEventSlim _applicationStarted = new ManualResetEventSlim();
ApplicationLifetime.ApplicationStarted.Register(() =>
{
     //bu sətiri əlavə edirik
    _applicationStarted.Set();
    Logger.LogInformation("Application started. Hosting environment: {envName}; Content root path: {contentRoot}", Environment.EnvironmentName, Environment.ContentRootPath);
});
protected override void OnStart(string[] args)
{
    _delayStart.TrySetResult(null);

     //bu sətiri əlavə edirik
    // Wait for host startup to complete before returning to SCM.
    _applicationStarted.Wait();

    base.OnStart(args);
}

Problem 2. Gözlənilməyən xəta baş verərkən servis əslində öz işini dayandırdı, lakin işlək vəziyyətdə qalırdı
Burda çaşdırıcı məsələ o idi ki, əsas işi görən Task xəta üzündən işini dayandırdı, lakin windows servis işlək vəziyyətdə qalırdı. Bunun üçün BackgroundService class-dan yeni bir class törədib bu class-da bəzi gəliştirmələr etdik

protected IHostApplicationLifetime ApplicationLifetime { get; }

protected CustomBackgroundService(IServiceProvider serviceProvider)
{
    ApplicationLifetime = serviceProvider.GetRequiredService<IHostApplicationLifetime>();
}

/// <summary>
/// Don't override this method, override <see cref="ExecuteCoreAsync"/> method instead
/// </summary>
/// <param name="stoppingToken"></param>
/// <returns></returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    try
    {
        await ExecuteCoreAsync(stoppingToken);
    }
    catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
    {
        Logger.LogInformation("Service successfully stopped");
    }
    catch (Exception exception)
    {
        Logger.LogCritical(exception, "There was unhandled exception and worker service will be stopped");                                         

        if (!stoppingToken.IsCancellationRequested)
        {
            ApplicationLifetime.StopApplication();
        }
    }
}

protected abstract Task ExecuteCoreAsync(CancellationToken stoppingToken);

ExecuteAsync metodunu override edib burada stoplama üçün ApplicationLifetime üzərindən StopApplication() metodunu çağırdıq və worker servisinin özündə isə ExecuteCoreAsync metodunu override edib işlətdik və məsələ həll olundu.

Xüsusi qeyd:

Əgər əməliyyat sisteminin birbaşa özünə aid olan APİ-lər istifadə etmisinizsə köhnə proyektinizdə keçid zamanı məsələn net5.0-windows və ya net5.0-macOS kimi target frameworkləri istifadə edə bilərsiniz. Bu framework-lər net5.0-da olan hər şey və xüsusi olaraq əməliyyat sisteminiz özünə adi olan API-ləri saxlayır.

Əgər siz ən son yenilikləri öz proyektlərinizdə tətbiq etmək və köhnə texnologiyalardan canınızı qurtarmaq istəyirsinizə məncə bir az əmək sərf edib çox da çətin olmayan bir şəkildə miqrasiya işinizi yekunlaşdıra bilərsiniz :)