مقداردهی اولیه Async lazy برای اشیاءمنقضی شده

، به عنوان بخشی از Net Framework. ما <Lazy<T را داریم ، که حمایتی را برای به تعویق انداختن ایجاد منابع بزرگ و یا منابع فشرده ، فراهم می‌آورد . حال اگر شی برای ایجاد شدن نیاز به عملیات async داشت ، و اگر مقدار آن بعد از یک مدت منقضی شد ، دوباره نیاز به یکسری انجام محاسبات دارد . که این یک مشکل است ، در این مقاله ما به ارائه راه حلی برای حل این مشکل خواهیم پرداخت .

مقداردهی اولیه Async lazy  برای اشیاءمنقضی شده

یک نمونه مورد استفاده ساده :
اجازه دهید سناریوی زیر را فرض کنیم ، 
شما در حال ایجاد یک سرویس تماس به یک HTTP API با استفاده از یک typical OAuth 2.0 client credentials flow می باشید . این بدان معناست که شما برای داشتن امکان استفاده از این برای برقراری تماس با Resource Server نیاز به فراهم آوردن یک Token از Identity Serrver دارید . بطور معمول بازیابی Token فقط یکبار مورد نیاز است ، و عملیات بازیابی token بهتر است که بصورت async باشد . در مجموع ، بر اساس OAuth spec ، جریان اعتبار کاربر از یک token تازه پشتیبانی نخواهد کرد ، پس زمانی که access token منقضی شود ، شما نیاز دارید که برای گرفتن یک Token جدید دوباره درخواست دهید .

تمام اینها بدین معناست که، ما با یک مقداردهی اولیه lazy سر و کار داریم (فقط یکبار token را میگیریم و در همان ابتدا با آن سر و کار داریم ) ، با کمک یک عملیات async و با یک object ای که منقضی خواهد شد .(پردازش و کار با token مدت زمان مشخصی دارد )

حال اجازه دهید به سراغ فراخوانی <AsyncExpiringLazy<T و پشتیبانی این سناریو در یک مسیر generic برویم .

ساخت یک <AsyncExpiringLazy<T :
در مجموع ، ما نیاز به یک Wrapper ساده برای محصور کردن نتیجه شی و زمان انقضای آن داریم . حال اجازه دهید به سراغ کد برویم :

public struct ExpirationMetadata<T>
{
    public T Result { get; set; }
 
    public DateTimeOffset ValidUntil { get; set; }
}

خوشبختانه ، این قسمت نیازی به توضیح آنچنانی ندارد . در مرحله بعد اجازه دهید به سراغ اضافه کردن AsyncExpiringLazy برویم . از زمانی که Property ها در #C نمیتوانند async باشند ، در AsyncExpiringLazy ما نیاز به استفاده از یک instance method تحت عنوان یک راه برای واکشی مقادیر مورد نیاز خواهیم داشت . 

ابتدا اجازه دهید که کلاس های بیرونی را ایجاد کرده و سپس در ادامه کار بدنه کلاس ها را پر خواهیم کرد :

public class AsyncExpiringLazy<T>
{
    public AsyncExpiringLazy(Func<ExpirationMetadata<T>, Task<ExpirationMetadata<T>>> valueProvider)
    {
        // TODO
    }
 
    public Task<bool> IsValueCreated()
    {
        // TODO
    }
 
    public async Task<T> Value()
    {
        // TODO
    }
 
    public Task Invalidate()
    {
        // TODO
    }
}

پس Constructor یک مقدار Provider دریافت خواهد کرد - خود Provider مقادیر قبلی/قدیمی یا حتی مقادیر منقضی شده (یا در حال انقضا) را دریافت میکند و مسئولیت فراهم آوردن یک مقدار جدید بر عهده او میباشد . محصور کردن آن در ExpirationMetadata زمان انقضای آن را تعریف خواهد کرد .

در کدهای ما فقط "trick" برا یlocking از SemaphoreSlim استفاده خواهد کرد -- از زمانی که #C امکان استفاده از جمله قدیمی lock را برای شامل شدن await ندارد .

 ما فقط می توانیم برای تنظیم مجدد ExpirationMetadata را به مقدار پیش فرض آن بازگردانیم . 

کد در زیر نشان داده شده است :

public class AsyncExpiringLazy<T>
{
    private static readonly SemaphoreSlim SyncLock = new SemaphoreSlim(initialCount: 1);
    private readonly Func<ExpirationMetadata<T>, Task<ExpirationMetadata<T>>> _valueProvider;
    private ExpirationMetadata<T> _value;

    public AsyncExpiringLazy(Func<ExpirationMetadata<T>, Task<ExpirationMetadata<T>>> valueProvider)
    {
        if (valueProvider == null) throw new ArgumentNullException(nameof(valueProvider));
        _valueProvider = valueProvider;
    }

    private bool IsValueCreatedInternal => _value.Result != null && _value.ValidUntil > DateTimeOffset.UtcNow;

    public async Task<bool> IsValueCreated()
    {
        await _syncLock.WaitAsync().ConfigureAwait(false);
        try
        {
            return IsValueCreatedInternal;
        }
        finally
        {
            _syncLock.Release();
        }
     }

    public async Task<T> Value()
    {
        await _syncLock.WaitAsync().ConfigureAwait(false);
        try
        {
            if (IsValueCreatedInternal)
            {
                return _value.Result;
            }
        }
        finally
        {
            _syncLock.Release();
        }

        await _syncLock.WaitAsync().ConfigureAwait(false);
        try
        {
            var result = await _valueProvider(_value).ConfigureAwait(false);
            _value = result;
            return _value.Result;
        }
        finally
        {
            _syncLock.Release();
        }
    }

    public async Task Invalidate()
    {
        await _syncLock.WaitAsync().ConfigureAwait(false);
        _value = default(ExpirationMetadata<T>);
        _syncLock.Release();
    }
}

روش استفاده :

استفاده از آن تقریبا ساده میباشد و در unit test بعدی نشان داده شده است :

// helper class for illustration
public class TokenResponse
{
    public string AccessToken { get; set; }
}

[Fact]
public async Task End2End()
{
    var testInstance = new AsyncExpiringLazy<TokenResponse>(async metadata =>
    {
        await Task.Delay(1000);
        return new ExpirationMetadata<TokenResponse>
        {
            Result = new TokenResponse
            {
                AccessToken = Guid.NewGuid().ToString()
            }, ValidUntil = DateTimeOffset.UtcNow.AddSeconds(2)
        };
    });

    // 1. check if value is created - shouldn't
    Assert.False(await testInstance.IsValueCreated());

    // 2. fetch lazy expiring value
    var token = await testInstance.Value();

    // 3a. verify it is created now
    Assert.True(await testInstance.IsValueCreated());

    // 3b. verify it is not null
    Assert.NotNull(token.AccessToken);

    // 4. fetch the value again. Since it's lifetime is 2 seconds, it should be still the same
    var token2 = await testInstance.Value();
    Assert.Same(token, token2);

    // 5. sleep for 2 seconds to let the value expire
    await Task.Delay(2000);

    // 6. fetch again
    var token3 = await testInstance.Value();

    // 7. verify we now have a new (recreated) value - as the previous one expired
    Assert.NotSame(token2, token3);

    // 8. invalidate the value manually before it has a chance to expire
    await testInstance.Invalidate();

    // 9. check if value is created - shouldn't anymore
    Assert.False(await testInstance.IsValueCreated());
}


شما میتوانید یک نمونه از AsyncExpiringLazy را در هر نقطه ای ایجاد کنید -- برای مثال این میتواند یک فیلد در یکی از کلاس های شما باشد . شما نیاز به ارسال یک delegate که مسئول مقدار ایجاد شده است را دارید -- همچنین این delegate این امکان را به شما می دهد که مقادیر قدیمی را در صورت نیاز بازرسی کنید . 

در مجموع ، همه ی اینها در برای انجام پردازش برای مقادیر در صورت نیاز است . و اگر این منقضی شده بود ، AsyncExpiringLazy در پردازش بعدی این را برای شما فراهم خواهد آورد .

دانلود نسخه ی PDF این مطلب