If i have a service that stores a token as a lazy object, and the token is fetched inside a value factory, is that value persisted across multiple requests, or since the underlying service is scoped, lazy starts with a null value for every single request?
_token = new Lazy<Token>(() =>
{
var token = new Token();
var task = Task.Run(async () =>
{
var response = await Login();
token.AccessToken = response.AccessToken;
});
task.Wait();
return token;
});
This is called in the constructor of the service:
public MyService()
{
private static Lazy<Token> _token
if (_token is { IsValueCreated: false }) // execute logic above
}
However, MyService is registered as scoped in Startup.cs,
services.AddScoped<IMyService, MyService>();
Does this mean that the token is fetched every time service is requested (despite the fact that value factory is thread safe) or the value factory can be called only once (even if it is called from within multiple scopes), since it's declared as static?
In general, any instance members of a service are bound to the lifetime of the service. So if the service itself is scoped, which in an ASP.NET Core context usually means that it only exists for the duration of a single request, then the members of that service will also only exist for that duration.
So your Lazy
value is being constructed during instantiation of MyService
and will be thrown away when the service goes out of scope, which happens at the end of the request.
If you want your member to persist for multiple requests, you would have to increase the lifetime of your service to singleton which means that you will only ever have one instance that is being reused. Note that this will also mean that everyone accessing your web application will end up being handled by the same service instance, so if your token is user-specific, this won’t work.
That being said, using a Lazy<T>
for an asynchronous process is not a good idea to begin with. By calling .Wait()
on the task, you are essentially throwing out every benefit of asynchronous execution while also opening yourself to potential deadlocks.
And also, just accessing IsValueCreated
on the Lazy object will not actually run the initialization logic of the Lazy if that was your goal in the constructor.
To properly utilize the asynchronous loading process, I would suggest you to change your service into something like this:
public class MyService
{
private Token _token;
public async Task<Token> GetToken()
{
if (_token is null)
{
_token = await LoadToken();
}
return _token;
}
private async Task<Token> LoadToken()
{
var token = new Token();
var response = await Login();
token.AccessToken = response.AccessToken;
token.RefreshToken = response.RefreshToken;
return token;
}
}
So instead of having a Lazy<Token>
property, you have an asynchronous method GetToken
which returns a Task<Token>
. That way, every consumer can properly await the token asychronously until it is loaded, and by storing the token, you still have a mechanism to cache it.
(Note that this solution – just like Lazy<T>
– is not thread-safe. If your service is a scoped service, it will likely not be consumed in parallel anyway though, so that should be fine.)