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

image

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).


image

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

image

Select View then SQL Server Object Explorer.


image

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.


image

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.


image

Close the window.


image

Run the project.


image

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.


image

We can hit the F12 key and open the web browser debug window and see there is an error.


Enabling Detailed Errors

image

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; });



image

When we run the application and enter a duplicate record, we get a much more informative error message.


Display Errors Using Try Catch

image

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.


image

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);
        }


image

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


image

Now when we run the application, and enter a duplicate Celsius value, the error message is displayed and the application does not hang.


image

However, we can navigate to the counter page…


image

… navigate back to the Fetch data page and enter a new Forecast


image

…enter a new Forecast that is not a duplicate…


image

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:

  1. Implement OwningComponentBase so the WeatherForecastService and EF Core do not remain active when we navigate to a different page in the application
  2. Clear ChangeTracker in EF Core when there are errors


Use OwningComponentBase

image

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);


image

Now we can enter a duplicate value


image

Navigate to a new page…


image

… return and add a new Forecast that is not a duplicate…


image

… and it will allow us to save.


image

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

image

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.

An error has occurred. This application may no longer respond until reloaded. Reload 🗙