Kode asynchronous di Startup ASP.NET Core: 4 cara untuk berkeliling GetAwaiter (). GetResult ()

Karena mekanisme async / await diperkenalkan pada C # 5.0, kami telah secara konstan diajarkan dalam semua artikel dan dokumen bahwa menggunakan kode asinkron dalam sinkron sangat buruk. Dan mereka memanggil ketakutan seperti konstruksi GetAwaiter (). GetResult (). Namun, ada satu kasus di mana programmer Microsoft sendiri tidak meremehkan desain ini.



Latar belakang tentang tugas pekerjaan


Kami sekarang dalam proses transisi dari otentikasi lama ke OAuth 2.0, yang sudah menjadi standar dalam industri kami. Layanan yang saya kerjakan sekarang telah menjadi percontohan untuk integrasi dengan sistem baru dan untuk transisi ke otentikasi JWT.

Selama proses integrasi, kami bereksperimen, mempertimbangkan berbagai opsi, cara mengurangi beban pada penyedia token (IdentityServer dalam kasus kami) dan memastikan keandalan yang lebih besar dari keseluruhan sistem. Menghubungkan validasi berbasis JWT ke ASP.NET Core sangat sederhana dan tidak terikat dengan implementasi spesifik dari penyedia token:

services
      .AddAuthentication()
      .AddJwtBearer(); 

Tapi apa yang tersembunyi di balik dua garis ini? Di bawah tenda mereka, JWTBearerHandler dibuat, yang sudah berurusan dengan JWT dari klien API.


Interaksi klien, API, dan penyedia token saat meminta

Ketika JWTBearerHandler menerima token dari klien, ia tidak mengirim token ke penyedia untuk validasi, melainkan meminta penyedia Signing Key - bagian publik dari kunci yang menandatangani token. Berdasarkan kunci ini, diverifikasi bahwa token ditandatangani oleh penyedia yang benar.

Di dalam JWTBearerHandler duduk HttpClient, yang berinteraksi dengan penyedia melalui jaringan. Tetapi, jika kami berasumsi bahwa Signing Key dari penyedia kami tidak berencana untuk sering berubah, maka Anda dapat mengambilnya sekali ketika Anda memulai aplikasi, cache diri Anda sendiri dan singkirkan permintaan jaringan yang konstan.

Saya mendapat kode ini untuk Signing Key:

public static AuthenticationBuilder AddJwtAuthentication(this AuthenticationBuilder builder, AuthJwtOptions options)
{
    var signingKeys = new List<SecurityKey>();

    var jwtBearerOptions = new JwtBearerOptions {Authority = options?.Authority};
    
    new JwtBearerPostConfigureOptions().PostConfigure(string.Empty, jwtBearerOptions);
    try
    {
        var config = jwtBearerOptions.ConfigurationManager
            .GetConfigurationAsync(new CancellationTokenSource(options?.AuthorityTimeoutInMs ?? 5000).Token)
            .GetAwaiter().GetResult();
        var providerSigningKeys = config.SigningKeys;
        signingKeys.AddRange(providerSigningKeys);
    }
    catch (Exception)
    {
        // ignored
    }

    builder
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                // ...
                IssuerSigningKeys = signingKeys,
                // ...
            };
        });
    return builder;
}

Pada baris 12 kita bertemu .GetAwaiter (). GetResult (). Itu karena AuthenticationBuilder dikonfigurasi di dalam ruang publik ConfigureServices (layanan IServiceCollection) {...} dari kelas Startup, dan metode ini tidak memiliki versi asinkron. Masalah.

Dimulai dengan C # 7.1, kami memiliki Main asynchronous (). Tetapi metode konfigurasi Startup tidak sinkron di Asp.NET Core belum dikirim. Saya secara estetika terganggu untuk menulis GetAwaiter (). GetResult () (saya diajari untuk tidak melakukan ini!), Jadi saya online untuk mencari cara orang lain menangani masalah ini.

Saya terganggu oleh GetAwaiter (). GetResult (), tetapi Microsoft tidak


Salah satu yang pertama saya menemukan opsi yang digunakan programmer Microsoft dalam tugas yang sama untuk mendapatkan rahasia dari Azure KeyVault . Jika Anda turun melalui beberapa lapisan abstraksi, maka kita akan melihat:

public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();

Halo lagi, GetAwaiter (). GetResult ()! Apakah ada solusi lain?

Setelah google singkat, saya menemukan serangkaian artikel hebat oleh Andrew Lock, yang setahun lalu memikirkan masalah yang sama dengan saya. Bahkan untuk alasan yang sama - ia secara estetis tidak suka secara serentak memohon kode asinkron.

Secara umum, saya merekomendasikan semua orang yang tertarik dengan topik ini untuk membaca seluruh seri dari lima artikel oleh Andrew. Di sana, ia menganalisis secara rinci tugas kerja yang mengarah ke masalah ini, kemudian mempertimbangkan beberapa pendekatan yang salah, dan baru kemudian menjelaskan solusi. Dalam artikel saya, saya akan mencoba menyajikan penelitian singkatnya, lebih berkonsentrasi pada solusi.

Peran tugas asinkron dalam memulai layanan web


Ambil langkah mundur untuk melihat seluruh gambar. Apa masalah konseptual yang saya coba pecahkan, terlepas dari kerangka kerjanya?
Masalah: perlu untuk memulai layanan web sehingga memproses permintaan kliennya, tetapi ada serangkaian operasi (yang relatif) lama, yang tanpanya layanan tidak dapat menanggapi klien, atau jawabannya tidak akan benar.
Contoh operasi tersebut:

  • Validasi konfigurasi sangat diketik.
  • Mengisi cache.
  • Koneksi awal ke database atau layanan lain.
  • Pemuatan JIT dan perakitan (pemanasan layanan).
  • Migrasi Basis Data. Ini adalah salah satu contoh Andrew Lock, tetapi dia sendiri mengakui bahwa bagaimanapun juga, operasi ini tidak diinginkan ketika memulai layanan .

Saya ingin menemukan solusi yang akan memungkinkan Anda untuk melakukan tugas asinkron sewenang-wenang pada startup aplikasi, dan dengan cara alami untuk mereka, tanpa GetAwaiter (). GetResult ().

Tugas-tugas ini harus diselesaikan sebelum aplikasi mulai menerima permintaan, tetapi untuk pekerjaan mereka, mereka mungkin memerlukan konfigurasi dan layanan terdaftar dari aplikasi. Oleh karena itu, tugas-tugas ini harus dilakukan setelah konfigurasi DI.

Ide ini dapat direpresentasikan dalam bentuk diagram:


Solusi # 1: solusi kerja yang dapat membingungkan ahli waris


Solusi kerja pertama yang ditawarkan oleh Lock :

public class Program
{
   public static async Task Main(string[] args)
   {
       IWebHost webHost = CreateWebHostBuilder(args).Build();

       using (var scope = webHost.Services.CreateScope())
       {
           //   
           var myService = scope.ServiceProvider.GetRequiredService<MyService>();

           await myService.DoAsyncJob();
       }

       await webHost.RunAsync();
   }

   public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
       WebHost.CreateDefaultBuilder(args)
           .UseStartup<Startup>();
}

Pendekatan ini dimungkinkan oleh munculnya Main asynchronous () dari C # 7.1. Satu-satunya negatif adalah bahwa kami mentransfer bagian konfigurasi dari Startup.cs ke Program.cs. Solusi non-standar untuk kerangka kerja ASP.NET dapat membingungkan seseorang yang akan mewarisi kode kami.

Solusi # 2: sematkan operasi asinkron dalam DI


Oleh karena itu, Andrew mengusulkan versi solusi yang lebih baik. Antarmuka untuk tugas asinkron dinyatakan:

public interface IStartupTask
{
    Task ExecuteAsync(CancellationToken cancellationToken = default);
}

Serta metode ekstensi yang mendaftarkan tugas-tugas ini di DI:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
        where T : class, IStartupTask
        => services.AddTransient<IStartupTask, T>();
}

Selanjutnya, metode ekstensi lain dideklarasikan, sudah untuk IWebHost:

public static class StartupTaskWebHostExtensions
{
    public static async Task RunWithTasksAsync(this IWebHost webHost, CancellationToken cancellationToken = default)
    {
        //      DI
        var startupTasks = webHost.Services.GetServices<IStartupTask>();

        //   
        foreach (var startupTask in startupTasks)
        {
            await startupTask.ExecuteAsync(cancellationToken);
        }

        //    
        await webHost.RunAsync(cancellationToken);
    }
}

Dan di Program.cs kita hanya mengubah satu baris. Sebagai gantinya:

await CreateWebHostBuilder(args).Build().Run();

Kami memanggil:

await CreateWebHostBuilder(args).Build().RunWithTasksAsync();

Menurut pendapat saya, pendekatan hebat yang membuat bekerja dengan operasi lama setransparan mungkin saat memulai aplikasi.

Solusi # 3: bagi mereka yang beralih ke ASP.NET Core 3.x


Tetapi jika Anda menggunakan ASP.NET Core 3.x, maka ada opsi lain. Saya akan kembali merujuk ke artikel oleh Andrew Lock .

Berikut adalah kode peluncuran WebHost dari ASP.NET Core 2.x:

public class WebHost
{
    public virtual async Task StartAsync(CancellationToken cancellationToken = default)
    {
        // ... initial setup
        await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);

        // Fire IApplicationLifetime.Started
        _applicationLifetime?.NotifyStarted();

        // Fire IHostedService.Start
        await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);

        // ...remaining setup
    }
}

Dan inilah metode yang sama di ASP.NET Core 3.0:

public class WebHost
{
    public virtual async Task StartAsync(CancellationToken cancellationToken = default)
    {
        // ... initial setup

        // Fire IHostedService.Start
        await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);

        // ... more setup
        await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);

        // Fire IApplicationLifetime.Started
        _applicationLifetime?.NotifyStarted();

        // ...remaining setup
    }
}

Dalam ASP.NET Core 3.x, HostedServices pertama kali diluncurkan dan hanya kemudian WebHost utama, dan sebelum itu justru sebaliknya. Apa yang ini berikan pada kita? Sekarang semua operasi asinkron dapat dipanggil di dalam metode StartAsync (CancellingToken) dari antarmuka IHostedService dan mencapai efek yang sama tanpa membuat antarmuka dan metode ekstensi yang terpisah.

Solusi # 4: kisah pemeriksaan kesehatan dan Kubernet


Seseorang bisa tenang dalam hal ini, tetapi ada pendekatan lain, dan tiba-tiba menjadi penting dalam kenyataan saat ini. Ini adalah penggunaan pemeriksaan kesehatan.

Ide dasarnya adalah memulai server Kestrel sedini mungkin untuk memberi tahu penyeimbang beban bahwa layanan siap menerima permintaan. Tetapi pada saat yang sama, semua permintaan pemeriksaan non-kesehatan akan mengembalikan 503 (Layanan Tidak Tersedia). Ada artikel yang cukup luas di situs Microsoft tentang cara menggunakan pemeriksaan kesehatan di ASP.NET Core. Saya ingin mempertimbangkan pendekatan ini tanpa rincian khusus sebagaimana diterapkan pada tugas kami.

Andrew Lock memiliki artikel terpisah untuk pendekatan ini. Keuntungan utamanya adalah ia menghindari timeout jaringan.
, , , . Kestrel , , « ».

Saya tidak akan memberikan di sini solusi lengkap Andrew Lock untuk pendekatan pemeriksaan kesehatan. Ini cukup banyak, tetapi tidak ada yang rumit di dalamnya.

Singkatnya, saya akan memberi tahu Anda: Anda harus memulai layanan web tanpa menunggu selesainya operasi asinkron. Dalam hal ini, titik akhir pemeriksaan kesehatan harus mengetahui status operasi-operasi ini, mengeluarkan 503 ketika sedang dieksekusi, dan 200 ketika mereka telah selesai.

Jujur, ketika saya mempelajari opsi ini, saya memiliki skeptisisme tertentu. Seluruh solusi tampak rumit dibandingkan dengan pendekatan sebelumnya. Dan jika kita menggambar analogi, maka ini adalah bagaimana menggunakan kembali pendekatan EAP dengan berlangganan acara, bukan async / tunggu yang sudah akrab.

Tapi kemudian Kubernetes ikut bermain. Dia memiliki konsep penyelidikan kesiapan sendiri. Saya akan mengutip dari buku "Kubernetes in Action" dalam presentasi gratis saya:
Selalu tentukan kesiapan pemeriksaan.

Jika Anda tidak memiliki probe kesiapan, pod Anda menjadi titik akhir layanan hampir secara instan. Jika aplikasi Anda terlalu lama bersiap untuk menerima permintaan masuk, permintaan klien untuk layanan ini juga akan memulai pod yang belum siap menerima koneksi masuk. Akibatnya, klien akan menerima kesalahan "Sambungan ditolak".

Saya melakukan percobaan sederhana: Saya membuat layanan ASP.NET Core 3 dengan tugas asinkron yang panjang di HostedService:

public class LongTaskHostedService : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
            Console.WriteLine("Long task started...");
            await Task.Delay(5000, cancellationToken);
            Console.WriteLine("Long task finished.");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {...}
}

Ketika saya memulai layanan ini menggunakan minikube, dan kemudian menambah jumlahnya menjadi dua, dalam waktu 5 detik dari keterlambatan, setiap permintaan saya yang kedua tidak menghasilkan informasi yang berguna, tetapi “Sambungan menolak”.

Kubernet 1.16 UPD
, , Kubernetes 1.16 startup probe ( ). , readiness probe. . .

temuan


Apa kesimpulan yang bisa ditarik dari semua studi ini? Mungkin setiap orang harus memutuskan untuk proyeknya solusi mana yang paling cocok. Jika diasumsikan bahwa operasi asinkron tidak akan mengambil terlalu banyak waktu, dan klien memiliki semacam kebijakan coba lagi, maka Anda dapat menggunakan semua pendekatan, mulai dengan GetAwaiter (). GetResult () dan berakhir dengan IHostedService di ASP.NET Core 3.x.

Di sisi lain, jika Anda menggunakan Kubernetes dan operasi asinkron Anda dapat dilakukan untuk waktu yang lama, maka Anda tidak dapat melakukannya tanpa pemeriksaan kesehatan (alias kesiapan / penyelidikan startup).

All Articles