9/16/2019 Admin
Blazor Error Handling OwningComponentBase and EF Core Tracking
The version of the book An Introduction to Building Applications with Blazor had a major change from Preview version 8 to Preview version 9 with the introduction of OwningComponentBase.
Using OwningComponentBase ensures that the service and related services that share its scope are disposed with the component. Otherwise a service will live for the life of user’s the connection to the application, which may be problematic if users stay connected for a long time.
To fully understand why this is important, we can explore what would happen when you don’t use OwningComponentBase. In addition, we will explore this issue in the larger context of handling errors in your Blazor applications and special code you need to implement when using Entity Framework Core.
Removing OwningComponentBase
We start with code from the article Creating A Step-By-Step End-To-End Database Server-Side Blazor Application (you can download the code from the downloads page on this site).
Open the project in Visual Studio 2019 Preview (or higher) and open the FetchData.razor page.
To remove OwningComponentBase, remove the line:
@inherits OwningComponentBase<WeatherForecastService>
And replace with:
@inject WeatherForecastService DataService
Replace all instances of @Service with DataService. For example replace:
forecasts = await @Service.GetForecastAsync(user.Identity.Name);
With
forecasts = await DataService.GetForecastAsync(user.Identity.Name);
Creating Errors
Select View then SQL Server Object Explorer.
Connect to the database that the project is connected to (see the article: Creating A Step-By-Step End-To-End Database Server-Side Blazor Application for help with this), right-click on the database, and select New Query.
In the query window, enter:
CREATE UNIQUE INDEX AK_WeatherForecast_TemperatureC ON WeatherForecast (TemperatureC); GO
…and press the execute button.
This creates a unique index that does not allow duplicates values for the TemperatureC field.
Close the window.
Run the project.
Create an Account, log in, and navigate to the Fetch data page.
Click the Add New Forecast button to create a Forecast.
Now, try to create a Forecast that has the same Celsius value as a previously saved Forecast.
The application will then hang.
We can hit the F12 key and open the web browser debug window and see there is an error.
Enabling Detailed Errors
To see a better error, we can close the web browser and stop the application, then open the Startup.cs file.
Then change the following line:
services.AddServerSideBlazor();
to:
services.AddServerSideBlazor() .AddCircuitOptions(options => { options.DetailedErrors = true; });
When we run the application and enter a duplicate record, we get a much more informative error message.
Display Errors Using Try Catch
Add a new class called DTOUpdateResult.cs using the following code:
namespace EndToEndDB.Data.EndToEnd { public class DTOUpdateResult { public DTOUpdateResult() { HasError = false; ErrorMessage = ""; } public bool HasError { get; set; } public string ErrorMessage { get; set; } } }
This creates a class that will be returned by the Create and Update methods to return any possible errors.
Open WeatherForecastService.cs and change the following methods as indicated (to return the new DTOUpdateResult class):
CreateForecastAsync
public Task<DTOUpdateResult> CreateForecastAsync(WeatherForecast objWeatherForecast) { DTOUpdateResult objDTOUpdateResult = new DTOUpdateResult(); try { _context.WeatherForecast.Add(objWeatherForecast); _context.SaveChanges(); } catch (System.Exception ex) { objDTOUpdateResult.HasError = true; objDTOUpdateResult.ErrorMessage = ex.GetBaseException().Message; } return Task.FromResult(objDTOUpdateResult); }
UpdateForecastAsync
public Task<DTOUpdateResult> UpdateForecastAsync(WeatherForecast objWeatherForecast) { DTOUpdateResult objDTOUpdateResult = new DTOUpdateResult(); try { var ExistingWeatherForecast = _context.WeatherForecast .Where(x => x.Id == objWeatherForecast.Id) .FirstOrDefault(); if (ExistingWeatherForecast != null) { ExistingWeatherForecast.Date = objWeatherForecast.Date; ExistingWeatherForecast.Summary = objWeatherForecast.Summary; ExistingWeatherForecast.TemperatureC = objWeatherForecast.TemperatureC; ExistingWeatherForecast.TemperatureF = objWeatherForecast.TemperatureF; _context.SaveChanges(); } else { objDTOUpdateResult.HasError = true; objDTOUpdateResult.ErrorMessage = "Record not found"; } } catch (System.Exception ex) { objDTOUpdateResult.HasError = true; objDTOUpdateResult.ErrorMessage = ex.GetBaseException().Message; } return Task.FromResult(objDTOUpdateResult); }
Open FetchData.razor.
Change the SaveForecast method to the following (to display any possible errors from the DTOUpdateResult class):
string ErrorMessage = ""; async Task SaveForecast() { DTOUpdateResult objDTOUpdateResult = new DTOUpdateResult(); ErrorMessage = ""; // Close the Popup ShowPopup = false; // Get the current user var user = (await authenticationStateTask).User; // A new forecast will have the Id set to 0 if (objWeatherForecast.Id == 0) { // Create new forecast WeatherForecast objNewWeatherForecast = new WeatherForecast(); objNewWeatherForecast.Date = System.DateTime.Now; objNewWeatherForecast.Summary = objWeatherForecast.Summary; objNewWeatherForecast.TemperatureC = Convert.ToInt32(objWeatherForecast.TemperatureC); objNewWeatherForecast.TemperatureF = Convert.ToInt32(objWeatherForecast.TemperatureF); objNewWeatherForecast.UserName = user.Identity.Name; // Save the result objDTOUpdateResult = DataService.CreateForecastAsync(objNewWeatherForecast).Result; } else { // This is an update objDTOUpdateResult = DataService.UpdateForecastAsync(objWeatherForecast).Result; } if(objDTOUpdateResult.HasError) { ErrorMessage = objDTOUpdateResult.ErrorMessage; } // Get the forecasts for the current user forecasts = await DataService.GetForecastAsync(user.Identity.Name); }
Finally add this code to the top of the page to display any error messages:
@ErrorMessage
Now when we run the application, and enter a duplicate Celsius value, the error message is displayed and the application does not hang.
However, we can navigate to the counter page…
… navigate back to the Fetch data page and enter a new Forecast …
…enter a new Forecast that is not a duplicate…
The update will not save!
The reason is that Entity Framework Core is still stuck on the original error. Navigating to the counter page and back demonstrates that EF Core, contained inside the WeatherForecastService, remains active no matter what page we go to in the application
We will address this situation in two parts:
- Implement OwningComponentBase so the WeatherForecastService and EF Core do not remain active when we navigate to a different page in the application
- Clear ChangeTracker in EF Core when there are errors
Use OwningComponentBase
Open FetchData.razor and change:
@inject WeatherForecastService DataService
To:
@inherits OwningComponentBase<WeatherForecastService>
Replace all instances of DataService with @Service. For example replace:
forecasts = await DataService.GetForecastAsync(user.Identity.Name);
With
forecasts = await @Service.GetForecastAsync(user.Identity.Name);
Now we can enter a duplicate value…
Navigate to a new page…
… return and add a new Forecast that is not a duplicate…
… and it will allow us to save.
However, if we get an error from a duplicate value, we cannot enter a new value, that is not a duplicate, unless we navigate away from the page and come back.
We need to clear the previous error in EF Core whenever there is an error.
Clear ChangeTracker in EF Tracking
Open WeatherForecastService.cs and add the following using statement to the top of the class:
using Microsoft.EntityFrameworkCore;
Next, add the following method:
public void DetachAllEntities() { var changedEntriesCopy = _context.ChangeTracker.Entries() .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted) .ToList(); foreach (var entry in changedEntriesCopy) entry.State = EntityState.Detached; }
Finally alter the catch block in the Create and Update methods to the following:
catch (System.Exception ex) { DetachAllEntities(); objDTOUpdateResult.HasError = true; objDTOUpdateResult.ErrorMessage = ex.GetBaseException().Message; }
Now we can enter a record that has an error, and then immediately enter a valid record, without the need to go to a new page and come back.
What If You Have Multiple Services?
See: An Introduction to OwningComponentBase
@inherits OwningComponentBase WeatherForecastService _WeatherForecastService; protected override async Task OnInitializedAsync() { // Get the current user var user = (await authenticationStateTask).User; _WeatherForecastService = (WeatherForecastService)ScopedServices.GetService(typeof(WeatherForecastService)); forecasts = await _WeatherForecastService.GetForecastAsync(user.Identity.Name); }
Links
An Introduction to OwningComponentBase
Utility base component classes to manage a DI scope
The Entity Framework Core ChangeTracker
How do I clear tracked entities in entity framework
Download
The project is available on the Downloads page on this site.
You must have Visual Studio 2019 (or higher) installed to run the code.