From 2c0f6f1caba1def1b9999cd87383a94eb2fe1faa Mon Sep 17 00:00:00 2001
From: James Chapman <jchapman3000@gmail.com>
Date: Sat, 12 Oct 2024 16:00:32 +0100
Subject: [PATCH] Fix error loading Add/Edit Transaction Item page

---
 Groceries/Home/HomePage.razor                    |  5 +++--
 Groceries/Items/ItemsPage.razor                  | 16 +++++++++++++---
 Groceries/Program.cs                             |  2 +-
 Groceries/Stores/StoreForm.razor                 |  5 +++--
 Groceries/Stores/StoresController.cs             | 12 +++++++++---
 Groceries/Stores/StoresPage.razor                | 14 ++++++++++++--
 .../Transactions/EditTransactionItemPage.razor   |  7 ++++---
 .../EditTransactionPromotionPage.razor           |  5 +++--
 .../Transactions/NewTransactionItemPage.razor    |  7 ++++---
 .../Transactions/NewTransactionItemsPage.razor   |  9 ++++++---
 Groceries/Transactions/NewTransactionPage.razor  |  5 +++--
 .../NewTransactionPromotionPage.razor            |  5 +++--
 .../NewTransactionPromotionsPage.razor           |  6 ++++--
 Groceries/Transactions/TransactionItemForm.razor |  7 ++++---
 .../Transactions/TransactionPromotionForm.razor  |  5 +++--
 Groceries/Transactions/TransactionsController.cs | 14 +++++++++++---
 Groceries/Transactions/TransactionsPage.razor    | 16 +++++++++++++---
 17 files changed, 99 insertions(+), 41 deletions(-)

diff --git a/Groceries/Home/HomePage.razor b/Groceries/Home/HomePage.razor
index a37f0d7..89ebaa4 100644
--- a/Groceries/Home/HomePage.razor
+++ b/Groceries/Home/HomePage.razor
@@ -4,7 +4,7 @@
 
 @layout Layout
 
-@inject AppDbContext DbContext
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 @inject IHttpContextAccessor HttpContextAccessor
 
 <HeadContent>
@@ -52,7 +52,8 @@
 
     protected override async Task OnInitializedAsync()
     {
-        model = await DbContext.ItemTagQuantities
+        using var dbContext = DbContextFactory.CreateDbContext();
+        model = await dbContext.ItemTagQuantities
             .FromSqlRaw(@"
                 SELECT tag, quantity, coalesce(unit_name, unit) AS unit, is_metric, is_divisible
                 FROM (
diff --git a/Groceries/Items/ItemsPage.razor b/Groceries/Items/ItemsPage.razor
index b5e8e98..938d0cd 100644
--- a/Groceries/Items/ItemsPage.razor
+++ b/Groceries/Items/ItemsPage.razor
@@ -2,7 +2,9 @@
 @using Microsoft.EntityFrameworkCore
 
 @layout Layout
-@inject AppDbContext DbContext
+
+@implements IDisposable
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <PageTitle>Groceries &ndash; Items</PageTitle>
 
@@ -44,6 +46,7 @@
         public DateTime? LastPurchasedAt { get; init; }
     }
 
+    private AppDbContext? dbContext;
     private IQueryable<ItemModel> items = null!;
     private PaginationState pagination = new();
 
@@ -52,7 +55,9 @@
 
     protected override void OnParametersSet()
     {
-        var itemsQuery = DbContext.Items.AsQueryable();
+        dbContext ??= DbContextFactory.CreateDbContext();
+
+        var itemsQuery = dbContext.Items.AsQueryable();
         if (!string.IsNullOrEmpty(Search))
         {
             var searchPattern = $"%{Search}%";
@@ -61,7 +66,7 @@
 
         items = itemsQuery
             .GroupJoin(
-                DbContext.ItemPurchases.Where(purchase => purchase.IsLastPurchase),
+                dbContext.ItemPurchases.Where(purchase => purchase.IsLastPurchase),
                 item => item.Id,
                 purchase => purchase.ItemId,
                 (item, purchases) => new { item, purchases })
@@ -78,4 +83,9 @@
             .OrderBy(item => item.Brand)
             .ThenBy(item => item.Name);
     }
+
+    public void Dispose()
+    {
+        dbContext?.Dispose();
+    }
 }
diff --git a/Groceries/Program.cs b/Groceries/Program.cs
index 5b0acb0..75d62c1 100644
--- a/Groceries/Program.cs
+++ b/Groceries/Program.cs
@@ -42,7 +42,7 @@ builder.Services.AddDistributedMemoryCache();
 builder.Services.AddHttpContextAccessor();
 builder.Services.AddSession();
 
-builder.Services.AddDbContextPool<AppDbContext>(options => options
+builder.Services.AddPooledDbContextFactory<AppDbContext>(options => options
     .EnableDetailedErrors(env.IsDevelopment())
     .EnableSensitiveDataLogging(env.IsDevelopment())
     .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
diff --git a/Groceries/Stores/StoreForm.razor b/Groceries/Stores/StoreForm.razor
index 5166e47..bfdd32e 100644
--- a/Groceries/Stores/StoreForm.razor
+++ b/Groceries/Stores/StoreForm.razor
@@ -1,7 +1,7 @@
 @using Groceries.Data
 @using Microsoft.EntityFrameworkCore
 
-@inject AppDbContext DbContext
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <form method="post" @attributes="AdditionalAttributes">
     <div class="form-field">
@@ -45,7 +45,8 @@
 
     protected override async Task OnInitializedAsync()
     {
-        retailers = await DbContext.Retailers
+        using var dbContext = DbContextFactory.CreateDbContext();
+        retailers = await dbContext.Retailers
             .OrderBy(retailer => retailer.Name)
             .ToArrayAsync();
     }
diff --git a/Groceries/Stores/StoresController.cs b/Groceries/Stores/StoresController.cs
index db2888e..5fbe151 100644
--- a/Groceries/Stores/StoresController.cs
+++ b/Groceries/Stores/StoresController.cs
@@ -8,11 +8,11 @@ using Microsoft.EntityFrameworkCore;
 [Route("/stores")]
 public class StoresController : Controller
 {
-    private readonly AppDbContext dbContext;
+    private readonly IDbContextFactory<AppDbContext> dbContextFactory;
 
-    public StoresController(AppDbContext dbContext)
+    public StoresController(IDbContextFactory<AppDbContext> dbContextFactory)
     {
-        this.dbContext = dbContext;
+        this.dbContextFactory = dbContextFactory;
     }
 
     [HttpGet]
@@ -32,6 +32,8 @@ public class StoresController : Controller
     [HttpPost("new")]
     public async Task<IResult> NewStore(Guid retailerId, string name, string? address)
     {
+        using var dbContext = dbContextFactory.CreateDbContext();
+
         var store = new Store(retailerId, name, address);
         dbContext.Stores.Add(store);
 
@@ -45,6 +47,8 @@ public class StoresController : Controller
     [HttpGet("edit/{id}")]
     public async Task<IResult> EditStore(Guid id)
     {
+        using var dbContext = dbContextFactory.CreateDbContext();
+
         var store = await dbContext.Stores
             .SingleOrDefaultAsync(store => store.Id == id, HttpContext.RequestAborted);
 
@@ -61,6 +65,8 @@ public class StoresController : Controller
     [HttpPost("edit/{id}")]
     public async Task<IResult> EditStore(Guid id, Guid retailerId, string name, string? address, string? returnUrl)
     {
+        using var dbContext = dbContextFactory.CreateDbContext();
+
         var store = new Store(id, retailerId, name, address);
         dbContext.Stores.Update(store);
 
diff --git a/Groceries/Stores/StoresPage.razor b/Groceries/Stores/StoresPage.razor
index 9581cde..0d32845 100644
--- a/Groceries/Stores/StoresPage.razor
+++ b/Groceries/Stores/StoresPage.razor
@@ -2,7 +2,9 @@
 @using Microsoft.EntityFrameworkCore
 
 @layout Layout
-@inject AppDbContext DbContext
+
+@implements IDisposable
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <PageTitle>Groceries &ndash; Stores</PageTitle>
 
@@ -39,6 +41,7 @@
         public int TransactionsCount { get; init; }
     }
 
+    private AppDbContext? dbContext;
     private IQueryable<StoreModel> stores = null!;
     private PaginationState pagination = new();
 
@@ -47,7 +50,9 @@
 
     protected override void OnParametersSet()
     {
-        var storesQuery = DbContext.Stores.AsQueryable();
+        dbContext ??= DbContextFactory.CreateDbContext();
+
+        var storesQuery = dbContext.Stores.AsQueryable();
         if (!string.IsNullOrEmpty(Search))
         {
             var searchPattern = $"%{Search}%";
@@ -66,4 +71,9 @@
             .OrderBy(store => store.Retailer)
             .ThenBy(store => store.Name);
     }
+
+    public void Dispose()
+    {
+        dbContext?.Dispose();
+    }
 }
diff --git a/Groceries/Transactions/EditTransactionItemPage.razor b/Groceries/Transactions/EditTransactionItemPage.razor
index a38985f..6eb71f5 100644
--- a/Groceries/Transactions/EditTransactionItemPage.razor
+++ b/Groceries/Transactions/EditTransactionItemPage.razor
@@ -3,7 +3,7 @@
 
 @layout Layout
 
-@inject AppDbContext DbContext
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <PageTitle>Groceries &ndash; Edit Transaction Item</PageTitle>
 
@@ -19,7 +19,7 @@
 
 <div class="row">
     <button class="button button--primary" type="submit" form="editTransactionItem">Update</button>
-    <a class="button" href="/transaction/new/items">Cancel</a>
+    <a class="button" href="/transactions/new/items">Cancel</a>
     <span class="row__fill"></span>
     <button class="button button--danger" type="submit" form="deleteTransactionItem">Remove</button>
 </div>
@@ -35,7 +35,8 @@
 
     protected override async Task OnParametersSetAsync()
     {
-        store = await DbContext.Stores
+        using var dbContext = DbContextFactory.CreateDbContext();
+        store = await dbContext.Stores
             .Where(store => store.Id == Transaction.StoreId)
             .Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
             .SingleAsync();
diff --git a/Groceries/Transactions/EditTransactionPromotionPage.razor b/Groceries/Transactions/EditTransactionPromotionPage.razor
index 768d24b..7ff4b9c 100644
--- a/Groceries/Transactions/EditTransactionPromotionPage.razor
+++ b/Groceries/Transactions/EditTransactionPromotionPage.razor
@@ -3,7 +3,7 @@
 
 @layout Layout
 
-@inject AppDbContext DbContext
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <PageTitle>Groceries &ndash; Edit Transaction Promotion</PageTitle>
 
@@ -35,7 +35,8 @@
 
     protected override async Task OnParametersSetAsync()
     {
-        store = await DbContext.Stores
+        using var dbContext = DbContextFactory.CreateDbContext();
+        store = await dbContext.Stores
             .Where(store => store.Id == Transaction.StoreId)
             .Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
             .SingleAsync();
diff --git a/Groceries/Transactions/NewTransactionItemPage.razor b/Groceries/Transactions/NewTransactionItemPage.razor
index 5873dc1..8ff73af 100644
--- a/Groceries/Transactions/NewTransactionItemPage.razor
+++ b/Groceries/Transactions/NewTransactionItemPage.razor
@@ -3,7 +3,7 @@
 
 @layout Layout
 
-@inject AppDbContext DbContext
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <PageTitle>Groceries &ndash; New Transaction Item</PageTitle>
 
@@ -16,7 +16,7 @@
 <TransactionItemForm TransactionItem="TransactionItem">
     <div class="row">
         <button class="button button--primary" type="submit">Add</button>
-        <a class="button" href="/transaction/new/items">Cancel</a>
+        <a class="button" href="/transactions/new/items">Cancel</a>
     </div>
 </TransactionItemForm>
 
@@ -31,7 +31,8 @@
 
     protected override async Task OnParametersSetAsync()
     {
-        store = await DbContext.Stores
+        using var dbContext = DbContextFactory.CreateDbContext();
+        store = await dbContext.Stores
             .Where(store => store.Id == Transaction.StoreId)
             .Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
             .SingleAsync();
diff --git a/Groceries/Transactions/NewTransactionItemsPage.razor b/Groceries/Transactions/NewTransactionItemsPage.razor
index ae0e22b..4a2bc68 100644
--- a/Groceries/Transactions/NewTransactionItemsPage.razor
+++ b/Groceries/Transactions/NewTransactionItemsPage.razor
@@ -2,7 +2,8 @@
 @using Microsoft.EntityFrameworkCore
 
 @layout Layout
-@inject AppDbContext DbContext
+
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <PageTitle>Groceries &ndash; New Transaction</PageTitle>
 
@@ -78,13 +79,15 @@
 
     protected override async Task OnParametersSetAsync()
     {
-        store = await DbContext.Stores
+        using var dbContext = DbContextFactory.CreateDbContext();
+
+        store = await dbContext.Stores
             .Where(store => store.Id == Transaction.StoreId)
             .Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
             .SingleAsync();
 
         var itemIds = Transaction.Items.Select(item => item.ItemId);
-        itemNames = await DbContext.Items
+        itemNames = await dbContext.Items
             .Where(item => itemIds.Contains(item.Id))
             .ToDictionaryAsync(item => item.Id, item => string.Concat(item.Brand, " ", item.Name));
     }
diff --git a/Groceries/Transactions/NewTransactionPage.razor b/Groceries/Transactions/NewTransactionPage.razor
index 46fd39a..f8cbb96 100644
--- a/Groceries/Transactions/NewTransactionPage.razor
+++ b/Groceries/Transactions/NewTransactionPage.razor
@@ -3,7 +3,7 @@
 
 @layout Layout
 
-@inject AppDbContext DbContext
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <PageTitle>Groceries &ndash; New Transaction</PageTitle>
 
@@ -41,7 +41,8 @@
 
     protected override async Task OnInitializedAsync()
     {
-        stores = await DbContext.Stores
+        using var dbContext = DbContextFactory.CreateDbContext();
+        stores = await dbContext.Stores
             .OrderBy(store => store.Retailer!.Name)
             .ThenBy(store => store.Name)
             .Select(store => new StoreModel(store.Id, string.Concat(store.Retailer!.Name, " ", store.Name)))
diff --git a/Groceries/Transactions/NewTransactionPromotionPage.razor b/Groceries/Transactions/NewTransactionPromotionPage.razor
index 1b29a8a..18970c1 100644
--- a/Groceries/Transactions/NewTransactionPromotionPage.razor
+++ b/Groceries/Transactions/NewTransactionPromotionPage.razor
@@ -3,7 +3,7 @@
 
 @layout Layout
 
-@inject AppDbContext DbContext
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <PageTitle>Groceries &ndash; New Transaction Promotion</PageTitle>
 
@@ -28,7 +28,8 @@
 
     protected override async Task OnParametersSetAsync()
     {
-        store = await DbContext.Stores
+        using var dbContext = DbContextFactory.CreateDbContext();
+        store = await dbContext.Stores
             .Where(store => store.Id == Transaction.StoreId)
             .Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
             .SingleAsync();
diff --git a/Groceries/Transactions/NewTransactionPromotionsPage.razor b/Groceries/Transactions/NewTransactionPromotionsPage.razor
index 8a86769..681699b 100644
--- a/Groceries/Transactions/NewTransactionPromotionsPage.razor
+++ b/Groceries/Transactions/NewTransactionPromotionsPage.razor
@@ -2,7 +2,8 @@
 @using Microsoft.EntityFrameworkCore
 
 @layout Layout
-@inject AppDbContext DbContext
+
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <PageTitle>Groceries &ndash; New Transaction</PageTitle>
 
@@ -64,7 +65,8 @@
 
     protected override async Task OnParametersSetAsync()
     {
-        store = await DbContext.Stores
+        using var dbContext = DbContextFactory.CreateDbContext();
+        store = await dbContext.Stores
             .Where(store => store.Id == Transaction.StoreId)
             .Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
             .SingleAsync();
diff --git a/Groceries/Transactions/TransactionItemForm.razor b/Groceries/Transactions/TransactionItemForm.razor
index f15e3ad..1ffba55 100644
--- a/Groceries/Transactions/TransactionItemForm.razor
+++ b/Groceries/Transactions/TransactionItemForm.razor
@@ -1,7 +1,7 @@
 @using Groceries.Data
 @using Microsoft.EntityFrameworkCore
 
-@inject AppDbContext DbContext
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <form method="post" @attributes="AdditionalAttributes">
     @* Ensure form action/method are used for implicit submission instead of barcode button *@
@@ -93,11 +93,12 @@
     {
         barcode = TransactionItem.Item?.Barcodes.FirstOrDefault();
 
-        items = await DbContext.Items
+        using var dbContext = DbContextFactory.CreateDbContext();
+        items = await dbContext.Items
             .OrderBy(item => item.Brand)
             .ThenBy(item => item.Name)
             .GroupJoin(
-                DbContext.ItemPurchases.Where(purchase => purchase.IsLastPurchase),
+                dbContext.ItemPurchases.Where(purchase => purchase.IsLastPurchase),
                 item => item.Id,
                 lastPurchase => lastPurchase.ItemId,
                 (item, purchases) => new { item, purchases })
diff --git a/Groceries/Transactions/TransactionPromotionForm.razor b/Groceries/Transactions/TransactionPromotionForm.razor
index a50f14b..80b4e83 100644
--- a/Groceries/Transactions/TransactionPromotionForm.razor
+++ b/Groceries/Transactions/TransactionPromotionForm.razor
@@ -1,7 +1,7 @@
 @using Groceries.Data
 @using Microsoft.EntityFrameworkCore
 
-@inject AppDbContext DbContext
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <form method="post" @attributes="AdditionalAttributes">
     <div class="form-field">
@@ -56,8 +56,9 @@
     {
         selectedItemIds = Promotion?.Items.Select(item => item.Id).ToArray() ?? [];
 
+        using var dbContext = DbContextFactory.CreateDbContext();
         var itemIds = Transaction.Items.Select(item => item.ItemId);
-        itemNames = await DbContext.Items
+        itemNames = await dbContext.Items
             .Where(item => itemIds.Contains(item.Id))
             .ToDictionaryAsync(item => item.Id, item => string.Concat(item.Brand, " ", item.Name));
     }
diff --git a/Groceries/Transactions/TransactionsController.cs b/Groceries/Transactions/TransactionsController.cs
index 5a2faf6..cdcd990 100644
--- a/Groceries/Transactions/TransactionsController.cs
+++ b/Groceries/Transactions/TransactionsController.cs
@@ -9,11 +9,11 @@ using System.Text.Json;
 [Route("/transactions")]
 public class TransactionsController : Controller
 {
-    private readonly AppDbContext dbContext;
+    private readonly IDbContextFactory<AppDbContext> dbContextFactory;
 
-    public TransactionsController(AppDbContext dbContext)
+    public TransactionsController(IDbContextFactory<AppDbContext> dbContextFactory)
     {
-        this.dbContext = dbContext;
+        this.dbContextFactory = dbContextFactory;
     }
 
     [HttpGet]
@@ -70,6 +70,8 @@ public class TransactionsController : Controller
         Item? item = null;
         if (barcodeData != null && barcodeFormat != null)
         {
+            using var dbContext = dbContextFactory.CreateDbContext();
+
             item = await dbContext.Items
                 .Where(item => item.Barcodes.Any(barcode => barcode.BarcodeData == barcodeData))
                 .OrderByDescending(item => item.UpdatedAt)
@@ -105,6 +107,8 @@ public class TransactionsController : Controller
             return Results.LocalRedirect("/transactions/new");
         }
 
+        using var dbContext = dbContextFactory.CreateDbContext();
+
         var itemId = await dbContext.Items
             .Where(item => EF.Functions.ILike(item.Brand, brand) && EF.Functions.ILike(item.Name, name))
             .Select(item => item.Id)
@@ -171,6 +175,8 @@ public class TransactionsController : Controller
             return Results.LocalRedirect("/transactions/new/items");
         }
 
+        using var dbContext = dbContextFactory.CreateDbContext();
+
         var itemId = await dbContext.Items
             .Where(item => EF.Functions.ILike(item.Brand, brand) && EF.Functions.ILike(item.Name, name))
             .Select(item => item.Id)
@@ -235,6 +241,8 @@ public class TransactionsController : Controller
             return Results.LocalRedirect("/transactions/new");
         }
 
+        using var dbContext = dbContextFactory.CreateDbContext();
+
         // Work around EF trying to insert items by explicitly tracking them as unchanged
         dbContext.Items.AttachRange(
             transaction.Items
diff --git a/Groceries/Transactions/TransactionsPage.razor b/Groceries/Transactions/TransactionsPage.razor
index 8c2f425..e98f103 100644
--- a/Groceries/Transactions/TransactionsPage.razor
+++ b/Groceries/Transactions/TransactionsPage.razor
@@ -1,7 +1,10 @@
 @using Groceries.Data
+@using Microsoft.EntityFrameworkCore
 
 @layout Layout
-@inject AppDbContext DbContext
+
+@implements IDisposable
+@inject IDbContextFactory<AppDbContext> DbContextFactory
 
 <PageTitle>Groceries &ndash; Transactions</PageTitle>
 
@@ -35,14 +38,16 @@
         public int TotalItems { get; init; }
     }
 
+    private AppDbContext? dbContext;
     private IQueryable<TransactionModel> transactions = null!;
     private PaginationState pagination = new();
 
     protected override void OnParametersSet()
     {
-        transactions = DbContext.Transactions
+        dbContext ??= DbContextFactory.CreateDbContext();
+        transactions = dbContext.Transactions
             .Join(
-                DbContext.TransactionTotals,
+                dbContext.TransactionTotals,
                 transaction => transaction.Id,
                 transactionTotal => transactionTotal.TransactionId,
                 (transaction, transactionTotal) => new TransactionModel
@@ -55,4 +60,9 @@
                 })
             .OrderByDescending(transaction => transaction.CreatedAt);
     }
+
+    public void Dispose()
+    {
+        dbContext?.Dispose();
+    }
 }