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

جمعه 26 آذر 1395

، به عنوان بخشی از 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 در پردازش بعدی این را برای شما فراهم خواهد آورد .

آموزش asp.net mvc

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید