Trent Scheffert
Trent Scheffert

Reputation: 199

Initialize Async Only Once Pattern

Let's say that I have a class with members which require asynchronous actions to initialize (such as file i/o or web requests). I only need to initialize once, and I don't want to reinitialize.

Are Tasks and Async-Await a good fit to accomplish this?

Here's an example of what I'm currently doing:

private Task _initializeTask;
public Task InitializeAsync()
{
    return _initializeTask ?? (_initializeTask = Task.Run(
            async () =>
            {
                // Do an action requiring await here
                await _storageField.LoadAsync();
            }));
}

Does this do what I think it does? Are there better ways to do it?

Is it thread safe? Not a requirement but should be considered.

Edits:

What I think it does? I believe that if _initializeTask hasn't been assigned then it will be assigned a new task that will kick off and then await the async lambda contained within. Any subsequent calls to the method will await the already running (or completed) task that was assigned to _initializedTask.

When do I want it to construct? Typically I'd use this sort of method on a service that I resolve with an IoC container. Multiple dependent classes can be constructed with a reference to the class. Then, before use, each of them awaits InitializeAsync(). If theres multiple dependent classes then I don't want to double up on initializing it.

Factory Method? There usually won't be multiple instances constructed that need to be initialized, so Factory method doesn't seem like a good solution. I've used something like a "static CreateAsync()" method for things like folder wrapper classes, but that didn't let me inject initialized folders into constructors. Async Factory methods don't gain anything when they can't be used with IoC constructor injection.

Upvotes: 3

Views: 4105

Answers (3)

Luke Oščádal
Luke Oščádal

Reputation: 71

If you work in a WebHost (ASP.NETCore app) or generic Host environment you can use simply way to do that with nuget HostInitActions

    public void ConfigureServices(IServiceCollection services)
    {       
        services.AddSingleton<IService, MyService>();

        services.AddAsyncServiceInitialization()
            .AddInitAction<IService>(async (service) =>
            {
                await service.InitAsync();
            });
    }

This nuget ensures that your initialization action will be performed asynchronously before the application starts.

Another advantage of this approach is that this initialization action can be defined from any place where services are installed into the IServiceCollection (For example, in an extension method in another project that installs internal implementations of public interfaces). This means that the ASP.NET Core project does not need to know what service and how it should be initialized, and it will still be done.

Upvotes: 0

NeddySpaghetti
NeddySpaghetti

Reputation: 13495

Your code will work but it is not thread safe, _initializeTask can be changed after checking it for null and before initializing it. This will result in two initializations. I would consider using AsyncLazy<T>, which is inherits from Lazy<T> which is thread safe.

Then assuming LoadAsync returns Task rather than Task<T>, your code becomes (untested):

private AsyncLazy<object> initializeTask = new AsyncLazy<object>(async () =>
            {
                // Do an action requiring await here
                await _storageField.LoadAsync();
                return null;
            });

public Task InitializeAsync()
{
    return _initializeTask.Value;
}

You can also define a non-generic version of `AsyncLazy, so you don't have to return a value from the initialization routine.

public class AsyncLazy : Lazy<Task> 
{ 
    public AsyncLazy(Func<Task> taskFactory) : 
        base(() => Task.Run(taskFactory)) { } 
}

Then you can initialize it using an initialization method, however that method is required to be static by the compiler:

private AsyncLazy _initializeTask = new AsyncLazy(LoadStorageAsync);

private static async Task LoadStorageAsync()
{   
    // Do an action requiring await here
    await _storageField.LoadAsync();
}   

public Task InitializeAsync()
{
    return _initializeTask.Value;
}

Upvotes: 5

Matt Meikle
Matt Meikle

Reputation: 41

Run the asynchronous task from a regular initialization function. In the regular function, check if your app already has a loaded data set that matches your expectations. If the data is not present THEN call the async function.

...
If (BooleanFunctionToDetermineIfDataIsNotPresent){FunctionToLoadFreshData}
...


Private Async Void FunctionToLoadFreshData{...}

The function to load the data must not return a value lest it become a task itself.

Upvotes: 0

Related Questions