رمز غير متزامن في Startup ASP.NET Core: 4 طرق للتنقل GetAwaiter (). GetResult ()

منذ تقديم آلية عدم التزامن / انتظار في C # 5.0 ، تم تعليمنا باستمرار في جميع المقالات والمستندات التي تعتبر استخدام رمز غير متزامن في متزامن أمرًا سيئًا للغاية. ويدعون إلى الخوف مثل الإنشاءات GetAwaiter (). GetResult (). ومع ذلك ، هناك حالة واحدة حيث لا يكره مبرمجو Microsoft أنفسهم هذا التصميم.



خلفية عن مهمة العمل


نحن الآن بصدد الانتقال من المصادقة القديمة إلى OAuth 2.0 ، وهو معيار قياسي بالفعل في صناعتنا. أصبحت الخدمة التي أعمل عليها الآن تجريبية للتكامل مع النظام الجديد والانتقال إلى مصادقة JWT.

أثناء عملية التكامل ، جربنا ، مع مراعاة الخيارات المختلفة ، كيفية تقليل الحمل على موفر الرمز المميز (IdentityServer في حالتنا) وضمان موثوقية أكبر للنظام بأكمله. إن ربط التحقق من صحة JWT بـ ASP.NET Core أمر بسيط للغاية ولا يرتبط بتنفيذ محدد لموفر الرمز المميز:

services
      .AddAuthentication()
      .AddJwtBearer(); 

لكن ما الذي يخفي وراء هذين الخطين؟ تحت غطاء محرك السيارة الخاص بهم ، يتم إنشاء JWTBearerHandler ، والذي يتعامل بالفعل مع JWT من عميل API.


تفاعل العميل وواجهة برمجة التطبيقات وموفر الرمز المميز عند الطلب

عندما يتلقى JWTBearerHandler الرمز المميز من العميل ، فإنه لا يرسل الرمز المميز إلى الموفر للتحقق من صحته ، بل يطلب موفر مفتاح التوقيع - الجزء العام من المفتاح الذي تم توقيع الرمز المميز معه. بناءً على هذا المفتاح ، تم التحقق من أن الرمز المميز موقّع من قبل الموفر الصحيح.

داخل JWTBearerHandler يجلس HttpClient ، الذي يتفاعل مع الموفر عبر الشبكة. ولكن ، إذا افترضنا أن مفتاح التوقيع الخاص بمزود الخدمة لدينا لا يخطط للتغيير في كثير من الأحيان ، فيمكنك استلامه مرة واحدة عند بدء التطبيق ، وتخزينها مؤقتًا والتخلص من طلبات الشبكة المستمرة.

حصلت على هذا الرمز لمفتاح التوقيع:

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;
}

في السطر 12 نلتقي .GetAwaiter (). GetResult (). ذلك لأنه تم تكوين AuthenticationBuilder داخل ConfigureServices الفارغ العام (خدمات IServiceCollection) {...} من فئة بدء التشغيل ، ولا تحتوي هذه الطريقة على إصدار غير متزامن. مشكلة.

بدءًا من C # 7.1 ، لدينا Main () غير متزامن. ولكن لم يتم تسليم طرق تكوين بدء التشغيل غير المتزامنة في Asp.NET Core حتى الآن. لقد انزعجت من الناحية الجمالية لكتابة GetAwaiter (). GetResult () (لقد علمت عدم القيام بذلك!) ، لذلك اتصلت بالإنترنت للبحث عن كيفية تعامل الآخرين مع هذه المشكلة.

أنا منزعج من GetAwaiter (). GetResult () ، لكن مايكروسوفت ليست كذلك


لقد وجدت أحد الخيارات الأولى التي استخدمها مبرمجو Microsoft في مهمة مماثلة للحصول على أسرار من Azure KeyVault . إذا ذهبت عبر عدة طبقات من التجريد ، فسوف نرى:

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

مرحبًا مرة أخرى ، GetAwaiter (). GetResult ()! هل هناك حلول أخرى؟

بعد جوجل قصير ، وجدت سلسلة من المقالات الرائعة التي كتبها أندرو لوك ، الذي فكر قبل عام في نفس المشكلة مثلي. حتى لنفس الأسباب - لا يحب جماليا استدعاء رمز غير متزامن بشكل متزامن.

بشكل عام ، أوصي كل من يهتم بهذا الموضوع بقراءة السلسلة الكاملة المكونة من خمس مقالات كتبها أندرو. هناك ، يحلل بالتفصيل مهام العمل التي تؤدي إلى هذه المشكلة ، ثم يدرس العديد من الأساليب غير الصحيحة ، ثم يصف الحلول فقط. في مقالتي سأحاول تقديم لمحة موجزة عن بحثه ، مع التركيز أكثر على الحلول.

دور المهام غير المتزامنة في بدء خدمة ويب


خذ خطوة للوراء لرؤية الصورة كاملة. ما هي المشكلة المفاهيمية التي حاولت حلها ، بغض النظر عن الإطار؟
المشكلة: من الضروري بدء تشغيل خدمة الويب بحيث تعالج طلبات عملائها ، ولكن هناك مجموعة من بعض العمليات المطولة (نسبيًا) ، والتي بدونها إما لا تستطيع الخدمة الاستجابة للعميل ، أو ستكون إجاباتها غير صحيحة.
أمثلة على مثل هذه العمليات:

  • التحقق من صحة التكوينات المكتوبة بشدة.
  • تعبئة ذاكرة التخزين المؤقت.
  • اتصال أولي بقاعدة البيانات أو الخدمات الأخرى.
  • JIT وتحميل التجميع (الاحماء الخدمة).
  • ترحيل قاعدة البيانات. هذا أحد أمثلة أندرو لوك ، لكنه يعترف بنفسه أنه بعد كل شيء ، هذه العملية غير مرغوب فيها عند بدء الخدمة .

أود أن أجد حلاً يسمح لك بأداء مهام عشوائية غير متزامنة عند بدء تشغيل التطبيق ، وبطريقة طبيعية لها ، بدون GetAwaiter (). GetResult ().

يجب إكمال هذه المهام قبل أن يبدأ التطبيق في قبول الطلبات ، ولكن لعملهم قد يحتاجون إلى التكوين والخدمات المسجلة للتطبيق. لذلك ، يجب تنفيذ هذه المهام بعد تكوين DI.

يمكن تمثيل هذه الفكرة في شكل مخطط:


الحل # 1: حل عملي يمكن أن يربك الورثة


أول حل عمل يقدمه 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>();
}

أصبح هذا النهج ممكنًا من خلال ظهور Main () غير المتزامن من C # 7.1. السلبية الوحيدة هي أننا نقلنا جزء التكوين من Startup.cs إلى Program.cs. مثل هذا الحل غير القياسي لإطار عمل ASP.NET يمكن أن يربك الشخص الذي سيرث التعليمات البرمجية الخاصة بنا.

الحل رقم 2: دمج العمليات غير المتزامنة في DI


لذلك ، اقترح أندرو نسخة محسنة من الحل. تم الإعلان عن واجهة للمهام غير المتزامنة:

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

بالإضافة إلى طريقة التمديد التي تسجل هذه المهام في DI:

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

بعد ذلك ، تم التصريح عن طريقة تمديد أخرى ، بالفعل لـ 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);
    }
}

وفي Program.cs نغير سطر واحد فقط. في حين أن:

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

نحن نتصل:

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

في رأيي ، نهج رائع يجعل العمل مع العمليات الطويلة شفافة قدر الإمكان عند بدء التطبيق.

الحل رقم 3: لأولئك الذين تحولوا إلى ASP.NET Core 3.x.


ولكن إذا كنت تستخدم ASP.NET Core 3.x ، فهناك خيار آخر. سأشير مرة أخرى إلى مقال بقلم أندرو لوك .

إليك رمز تشغيل WebHost من 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
    }
}

وإليك نفس الطريقة في 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
    }
}

في ASP.NET Core 3.x ، تم إطلاق HostedServices لأول مرة ، وبعد ذلك فقط WebHost الرئيسي ، وفي وقت سابق كان العكس تمامًا. ماذا يعطينا هذا؟ يمكن الآن استدعاء جميع العمليات غير المتزامنة داخل طريقة StartAsync (CancellationToken) للواجهة IHostedService وتحقيق نفس التأثير دون إنشاء واجهات منفصلة وطرق تمديد.

الحل # 4: قصة الفحص الصحي و Kubernetes


يمكن للمرء أن يهدأ من هذا ، ولكن هناك نهج آخر ، وتبين فجأة أنه مهم في الحقائق الحالية. هذا هو استخدام الفحص الصحي.

تتمثل الفكرة الأساسية في بدء تشغيل خادم Kestrel في أقرب وقت ممكن لإبلاغ موازن التحميل أن الخدمة جاهزة لقبول الطلبات. ولكن في الوقت نفسه ، ستُرجع جميع طلبات الفحص غير الصحي 503 (الخدمة غير متاحة). هناك مقالة شاملة إلى حد ما على موقع Microsoft حول كيفية استخدام الفحص الصحي في ASP.NET Core. كنت أرغب في النظر في هذا النهج دون تفاصيل خاصة كما هو مطبق على مهمتنا.

أندرو لوك لديه مقال منفصل لهذا النهج. ميزته الرئيسية هي أنه يتجنب مهلات الشبكة.
, , , . Kestrel , , « ».

لن أعطي هنا حل أندرو لوك الكامل لنهج الفحص الصحي. إنه ضخم للغاية ، ولكن لا يوجد شيء معقد فيه.

سأقول لك باختصار: تحتاج إلى بدء خدمة الويب دون انتظار اكتمال العمليات غير المتزامنة. في هذه الحالة ، يجب أن تكون نقطة نهاية الفحص الصحي على دراية بحالة هذه العمليات ، وإصدار 503 أثناء تنفيذها ، و 200 عند الانتهاء بالفعل.

بصراحة ، عندما درست هذا الخيار ، كان لدي بعض الشك. بدا الحل كله مرهقًا مقارنة بالمقاربات السابقة. وإذا قمنا برسم تشابه ، فهذه هي كيفية استخدام نهج EAP مرة أخرى مع اشتراك الحدث ، بدلاً من غير المتزامن / المنتظر بالفعل.

ولكن بعد ذلك بدأ Kubernetes في اللعب. لديه مفهومه الخاص للتحقيق الجاهزية. سأقتبس من كتاب "Kubernetes in Action" في عرضي المجاني:
حدد دائمًا مسبار الجاهزية.

إذا لم يكن لديك مسبار جاهز ، تصبح قرونك نقاط نهاية للخدمات على الفور تقريبًا. إذا كان التطبيق الخاص بك يستغرق وقتًا طويلاً في التحضير لقبول الطلبات الواردة ، فإن طلبات العملاء للخدمة ستبدأ أيضًا في تشغيل حوامل غير جاهزة لقبول الاتصالات الواردة. نتيجة لذلك ، سيتلقى العملاء خطأ "تم رفض الاتصال".

أجريت تجربة بسيطة: لقد أنشأت خدمة ASP.NET Core 3 بمهمة طويلة غير متزامنة في 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)
    {...}
}

عندما بدأت هذه الخدمة باستخدام minikube ، ثم قمت بزيادة العدد إلى اثنين ، في غضون 5 ثوانٍ من التأخير ، لم ينتج عن كل طلب ثانٍ معلومات مفيدة ، ولكن "تم رفض الاتصال".

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

الموجودات


ما الاستنتاج الذي يمكن استخلاصه من كل هذه الدراسات؟ ربما يجب على الجميع أن يقرروا لمشروعه الحل الأنسب. إذا كان من المفترض أن العملية غير المتزامنة لن تستغرق وقتًا طويلاً ، وأن العملاء لديهم نوعًا من سياسة إعادة المحاولة ، فيمكنك حينئذٍ استخدام جميع الأساليب ، بدءًا من GetAwaiter (). GetResult () وانتهاءً بـ IHostedService في ASP.NET Core 3.x.

من ناحية أخرى ، إذا كنت تستخدم Kubernetes ويمكن إجراء عملياتك غير المتزامنة لفترة طويلة بشكل ملحوظ ، فلا يمكنك الاستغناء عن الفحص الصحي (المعروف أيضًا باسم مسبار الجاهزية / بدء التشغيل).

All Articles