blazorparent-childdbcontextdatabase-concurrency

Blazor Parent and child OnInitializedAsync accessing DB context at same time


Both parent and child have to access db context in order to get their specific data, bellow is their code.

Parent:

[Inject]
private IProductsService ProductService { get; set; }
private IEnumerable<ProductModel> ProdList;      
private bool FiltersAreVisible = false;

protected override async Task OnInitializedAsync()
{
  ProdList = await ProductService.GetObjects(null);            
}

Child:

[Parameter]
public IEnumerable<ProductModel> ProdList { get; set; }
[Parameter]
public EventCallback<IEnumerable<ProductModel>> ProdListChanged { get; set; } 
[Inject]
private IRepositoryService<ProdBusinessAreaModel> ProdBAreasService { get; set; }
[Inject]
private IRepositoryService<ProdRangeModel> ProdRangesService { get; set; }
[Inject]
private IRepositoryService<ProdTypeModel> ProdTypesService { get; set; }
[Inject]
private IProductsService ProductService { get; set; }        
private ProductFilterModel Filter { get; set; } = new ProductFilterModel();
private EditContext EditContext;
private IEnumerable<ProdBusinessAreaModel> ProdBAreas;
private IEnumerable<ProdRangeModel> ProdRanges;
private IEnumerable<ProdTypeModel> ProdTypes;

protected override async Task OnInitializedAsync()
{
  EditContext = new EditContext(Filter);            
  EditContext.OnFieldChanged += OnFieldChanged;

  ProdBAreas = await ProdBAreasService.GetObjects();
  ProdRanges = await ProdRangesService.GetObjects();
  ProdTypes = await ProdTypesService.GetObjects();
}

This is throwing the following exception: InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext.

Using break points I see that parent runs OnInitializedAsync and when reaches ProdList = await ProductService.GetObjects(null); jumps right away to child OnInitializedAsync.

I solved it by making all the requests from parent and then passing to child but I wonder if there is a better way to do this, leaving child with the ability to get its own data and of course without making DB context Transient..

Regards


Solution

  • You should implement the DbContext factory in order to prevent a situation when two or more units of work for the same request compete for the same resources. See code sample below how to do that. Generally speaking, you should always implement the DbContext factory... However, it is a better code design to retrieve your data from a single location, as for instance, from your parent component, and pass it to its child component in the form of parameters. Still better, it is a good idea to create a service that implement the State and Notify patterns to provide data to interested components, notify them of changes, and generally manage and handle everything related to data. The FlightFinder Blazor App sample created by maestro Steve Anderson is a good example how to do that. However, you should follow your heart, and code as you wish. I'm just pointing out the recommended patterns.

    Here's the code sample you can preview and adapt into your app:

    ContactContext.cs

    /// <summary>
        /// Context for the contacts database.
        /// </summary>
        public class ContactContext : DbContext
        {
            /// <summary>
            /// Magic string.
            /// </summary>
            public static readonly string RowVersion = nameof(RowVersion);
    
            /// <summary>
            /// Magic strings.
            /// </summary>
            public static readonly string ContactsDb = nameof(ContactsDb).ToLower();
    
            /// <summary>
            /// Inject options.
            /// </summary>
            /// <param name="options">The <see cref="DbContextOptions{ContactContext}"/>
            /// for the context
            /// </param>
            public ContactContext(DbContextOptions<ContactContext> options)
                : base(options)
            {
                Debug.WriteLine($"{ContextId} context created.");
            }
    
            /// <summary>
            /// List of <see cref="Contact"/>.
            /// </summary>
            public DbSet<Contact> Contacts { get; set; }
    
            /// <summary>
            /// Define the model.
            /// </summary>
            /// <param name="modelBuilder">The <see cref="ModelBuilder"/>.</param>
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                // this property isn't on the C# class
                // so we set it up as a "shadow" property and use it for concurrency
                modelBuilder.Entity<Contact>()
                    .Property<byte[]>(RowVersion)
                    .IsRowVersion();
    
                base.OnModelCreating(modelBuilder);
            }
    
            /// <summary>
            /// Dispose pattern.
            /// </summary>
            public override void Dispose()
            {
                Debug.WriteLine($"{ContextId} context disposed.");
                base.Dispose();
            }
    
            /// <summary>
            /// Dispose pattern.
            /// </summary>
            /// <returns>A <see cref="ValueTask"/></returns>
            public override ValueTask DisposeAsync()
            {
                Debug.WriteLine($"{ContextId} context disposed async.");
                return base.DisposeAsync();
            }
        } 
    

    ConfigureServices

     // register factory and configure the options
                #region snippet1
                services.AddDbContextFactory<ContactContext>(opt =>
                    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
                    .EnableSensitiveDataLogging());
                #endregion 
    

    Here's how you inject it into your component:

    @inject IDbContextFactory<ContactContext> DbFactory
    

    And here's a code sample how to use it:

    using var context = DbFactory.CreateDbContext();
    
            // this just attaches
            context.Contacts.Add(Contact);
    
            try
            {
                await context.SaveChangesAsync();
                Success = true;
                Error = false;
                // ready for the next
                Contact = new Contact();
                Busy = false;
            }
            catch (Exception ex)
            {
                Success = false;
                Error = true;
                ErrorMessage = ex.Message;
                Busy = false;
            }
    

    UPDATE:

    Parent passing to child data and using only one context throught the hole scope is how much better performing then DB context Factory?

    First off, you should implement the DbContext factory in any case, right !? Once again, I do not suggest to use "Parent passing...the hole scope" instead of implementing the DbContext factory. In Blazor you must implement the DbContext factory resource racing. OK. But It is also recommended to expose your data from a single location: be it a service or a parent component. In the Component Model used in framework like Angular and Blazor, data is usually flows downstream, from a parent to its child. I'm sure you saw many code samples that do that, and this is how you should code.