Search code examples
c#mvvmlazy-initialization

Am I using in a wrong way async lazy initialization?


I have a view model with 2 collections, that takes the data from database. For example countries and type of VAT.

I would like to defer the initialization of this collections until I will, becauase if I am not wrong, it is better don't do it in the constructor becuase it is a high cost resource and the constrcutor has to be cheap to create.

So one option that I am trying to use is lazy initialization. this is the documentation.

But I am using a little bit different code, because the methods that get the data from the database are async.

This is my view model:

public class MyViewModel
{
    //Service to get data from database.
    private readonly IDataService _dataService;



    //The service is injected with dependency injection.
    public MyViewModel(IDataService paramDataService)
    {
        _dataService = paramDataService;


        _countriesoOc = new Lazy<Task<ObservableCollection<Contry>>>(InitializeContriesAsync);
        _vatsoOc = new Lazy<Task<ObservableCollection<VAT>>>(InitializeVatsAsync);
    }



    private readonly Lazy<Task<ObservableCollection<Country>>> _countriesOc;
    public IReadOnlyCollection<Cauntry> Coutries => _countriesOc.Value.Result;

    public Country? SelectedCountry;


    private async Task<ObservableCollection<MagnitudDTO>> InitializeCountriesAsync()
    {
        //Tset long process
        await Task.Delay(5000).ConfigureAwait(false);
        return new ObservableCollection<Contry>(await _dataService.GetAllContriesAsync().ConfigureAwait(false));
    }



    private readonly Lazy<Task<ObservableCollection<VAT>>> _vatsOc;
    public IReadOnlyCollection<VAT> Vats => _vatsOc.Value.Result;

    public VAT? SelectedVat;


    private async Task<ObservableCollection<VAT>> InitializeVatsAsync()
    {
        //Tset long process
        await Task.Delay(5000).ConfigureAwait(false);
        return new ObservableCollection<VAT>(await _dataService.GetAllVatsAsync().ConfigureAwait(false));
    }



    public void SetInvoce(Invoce paramInvoce)
    {
        SelectedCountry = _countriesOc.FirstOrDefault(x => x.Id = paramInvoce.IdCountry);
        SelectedVat = _vatsOc.FirstOrDefault(x => x.Id = paramInvoce.IdVat);
    }
}

I have realize that in this case, when I set the first invoice, it takes 10 seconds, because it seems when it set the country it waits the 5 seconds of the initialization and then continue with the set of the VAT, that takes another 5 seconds.

There is some way to set the selected items in a parallel way? I guess that perhaps I am doing wrong using the .Result property in the collections, but I am not sure.

I would try another option, that is using an intilize method that run all the methods in parallel. This is the solution:

public class MyViewModel
{
    //Service to get data from database.
    private readonly IDataService _dataService;



    //The service is injected with dependency injection.
    public MyViewModel(IDataService paramDataService)
    {
        _dataService = paramDataService;
    }


    public Task InitializeAsync()
    {
        return Task.WhenAll(IntializeCountriesAsync(), InitializeVatsAsync());
    }


    private readonly ObservableCollection<Country> _countriesOc;
    public IReadOnlyCollection<Cauntry> Coutries => _countriesOc;

    public Country? SelectedCountry;


    private async Task<ObservableCollection<MagnitudDTO>> InitializeCountriesAsync()
    {
        //Tset long process
        await Task.Delay(5000).ConfigureAwait(true);
        _countriesOc.AddRange(await _dataService.GetAllCountriesAsync().ConfigureAwait(true));
    }



    private readonly Lazy<Task<ObservableCollection<VAT>>> _vatsOc;
    public IReadOnlyCollection<VAT> Vats => _vatsOc.Value.Result;

    public VAT? SelectedVat;


    private async Task<ObservableCollection<VAT>> InitializeVatsAsync()
    {
        //Tset long process
        await Task.Delay(5000).ConfigureAwait(true);
        _vatsOc.AddRange(await _dataService.GetAllVatsAsync().ConfigureAwait(true));
    }



    public void SetInvoce(Invoce paramInvoce)
    {
        SelectedCountry = _countriesOc.FirstOrDefault(x => x.Id = paramInvoce.IdCountry);
        SelectedVat = _vatsOc.FirstOrDefault(x => x.Id = paramInvoce.IdVat);
    }
}

In this case, each object that use the view model, get the instance from dependency injection, but it has to call to the initializeAsync() method to populate the collections.

The advange of this solution it is that the initialization takes 5 seconds, because all the collections are initialize in parallel. But I don't like that each object that consume the view model, has to initialize it. It is less transparent for the consumer that the async lazy solution, but it is much faster. If I have many collections, they are initialized in parallel.

So in sumary, I would like to know if there is some way to improve the async lazy solution to run the initialization in parallel, if not, I guess i would go for the second solution, although each consumer has to call the initalize method always before can use it.

Thanks.


Solution

  • The second option could be rewritten as:

    public class MyViewModel
    {
        private readonly IDataService _dataService;
        private readonly Lazy<Task> InitTask;
    
        public MyViewModel(IDataService paramDataService)
        {
            _dataService = paramDataService;
            InitTask = new Lazy<Task>(Task.WhenAll(IntializeCountriesAsync(), InitializeVatsAsync()));
        }
    
        // ...
        public void SetInvoce(Invoce paramInvoce)
        {
            InitTask.Value.Wait();
            SelectedCountry = _countriesOc.FirstOrDefault(x => x.Id = paramInvoce.IdCountry);
            SelectedVat = _vatsOc.FirstOrDefault(x => x.Id = paramInvoce.IdVat);
        }
    }
    

    Though personally I would highly recommend to follow the advice to don't block on async code and make SetInvoce async too:

    public async Task SetInvoce(Invoce paramInvoce)
    {
        await InitTask.Value.ConfigureAwait(false);
        SelectedCountry = _countriesOc.FirstOrDefault(x => x.Id = paramInvoce.IdCountry);
        SelectedVat = _vatsOc.FirstOrDefault(x => x.Id = paramInvoce.IdVat);
    }
    

    If this is an option though, then I would discard the "sync wrappers" of the first case and just do something like the following:

    public async Task SetInvoce(Invoce paramInvoce)
    {
        await Task.WhenAll(_countriesoOc.Value, _vatsoOc.Value).ConfigureAwait(false);
        SelectedCountry = _countriesoOc.Result.FirstOrDefault(x => x.Id = paramInvoce.IdCountry);
        SelectedVat = _vatsOc.Result.FirstOrDefault(x => x.Id = paramInvoce.IdVat);
    }
    

    Alternatively you could look into utilizing in-memory cache.