Version Information
- .NET SDK 5.0.104
- ASP.NET Core Blazor WebAssembly: 5.0.4
- MudBlazor: 5.0.4
Der gesamte Source Code zur Beispielanwendung findet sich in diesem GitHub Repository.
Exception Handling als Aufgabenstellung
Der richtige Umgang mit Fehlern ist für die Erstellung einer robusten Anwendung in Blazor WebAssembly von entscheidender Bedeutung. Fehlerbehandlungsroutinen bieten die Möglichkeit, dass wir benutzerfreundliche Informationen präsentieren und wichtige Daten für die Entwicklung sammeln können. Im Zeitalter von fortgeschrittenen Front-End-Web-Apps ist es wichtiger denn je, eine effektive, clientseitige Lösung für die Fehlerbehandlung zu haben.
Eine Anwendung, die Fehler nicht ordnungsgemäß behandelt, lässt ihre Benutzer verwirrt und frustriert zurück, wenn die App plötzlich ohne Erklärung unterbrochen wird. Der korrekte Umgang mit diesen Fehlern in einer Anwendung verbessert die Benutzererfahrung erheblich. Gesammelte Daten aus der Fehlerbehandlung können das Entwicklungsteam über wichtige, nach dem Testen aufgetretene Probleme informieren.
Treten in einer Blazor WebAssembly SPA unbehandelte Fehler auf, kann dies fatale Folgen haben für die weitere Nutzung der Anwendung. Denn der Benutzer kann nur fortfahren und die Webanwendung weiter nutzen, indem er die Seite neu lädt. Bei einem nicht behobenen Fehler, wird am unteren Rand der Seite eine Meldung angezeigt: An unhandled error has occurred.
. Die Meldung fordert den Nutzer dann auf die Seite neu zu laden.
Würde das Framework versuchen, trotz des Fehlers weiterzumachen, könnte nicht gewährleistet werden, dass die Anwendung weiter korrekt funktioniert. Je nach Aufbau der Webanwendung kann es sogar zu Sicherheitsproblemen kommen. Um dies zu beheben, müssen wir für das Auftreten von unbehandelten Fehlern eine geeignete Fehlerbehebung einbauen.
Lassen Sie uns also schauen, wie wir dies mit Blazor WebAssembly realisieren können. Denn auch in .NET 5 ist ein Global Exception Handling nicht offiziell Teil des Blazor APIs.
Global Exception Handling im .NET-Code
Um nicht behandelte Fehler im .NET-Code abfangen zu können, gibt es derzeit noch keine allgemeine Lösung. Ein Weg globale Exceptions abzufangen, ist einen eigenen LoggingProvider
zu implementieren. Dadurch haben wir die sobald ein Log geschrieben wird die Möglichkeit zu prüfen, um welche Art von Exception es sich handelt. Bei einer Exception die einen Reload der Anwendung benötigt, kann durch eine eigene Implementierung des ILogger
-Interface darauf reagiert werden, wie im folgenden Code-Beispiel zu sehen:
public class CriticalExceptionLoggingProvider : ILoggerProvider
{
private CriticalExceptionLogger _logger;
public CriticalExceptionLoggingProvider(NavigationManager navigationManager)
{
_logger = new CriticalExceptionLogger(navigationManager);
}
public void Dispose()
{
_logger = null;
}
public ILogger CreateLogger()
{
if (_logger != null)
{
return _logger;
}
return NullLogger.Instance;
}
}
public class CriticalExceptionLogger : ILogger
{
private readonly NavigationManager _navigationManager;
public CriticalExceptionLogger(NavigationManager navigationManager)
{
_navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
}
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func formatter)
{
// Log Exception in einem log-File
if (logLevel == LogLevel.Critical)
{
_navigationManager.NavigateTo(_navigationManager.BaseUri, true);
}
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel == LogLevel.Error;
}
public IDisposable BeginScope(TState state)
{
return null;
}
}
Im obigen Code-Beispiel wird ein eigener LoggingProvider
implementiert. Dieser gibt für das ILogger
-Interface eine Instanz des CriticalExceptionLogger
zurück. Der CriticalExceptionLogger
prüft den Status der geworfenen Exception. Handelt es sich hier um einen fatalen Fehler mit dem LogLevel Critical
, kann die SPA voraussichtlich nicht mehr weiter genutzt werden. Daher müssen wir im nächsten Schritt einen Reload der Anwendung erzwingen, um dem Nutzer die Möglichkeit zu geben, die Anwendung neuerlich nutzen zu können.
Mit Hilfe des NavigationManager
, welchen wir per Dependency Injection dem CriticalExceptionLogger
verfügbar machen, kann die aktuelle Seite der Anwendung neu geladen werden. Da es sich hier um Sample-Code handelt, wird die Exception vor dem Reload der Anwendung nicht weiterverarbeitet. Jedoch wäre es sinnvoll die Informationen aller Exception abzuspeichern oder an ein Web API zu senden, um im Nachgang den Fehler auslesen und beheben zu können.
Um den LoggingProvider
in unserer Anwendung nutzen zu können, muss dieser in der ServiceCollection
registriert werden.
builder.Services.AddSingleton(services =>
{
var navigationManager = services.GetService();
return new CriticalExceptionLoggingProvider(navigationManager);
});
Doch nicht nur im .NET Core-Code können unbehandelte Fehler geworfen werden. Auch im JavaScript-Code können diese auftreten und müssen adressiert werden. Im nächsten Abschnitt schauen wir uns an, wie Sie diese Fehler abfangen und im .NET-Code weiter verarbeiten können.
Global Exception Handling im JavaScript-Code
Aufruf einer .NET-Methode mit Hilfe von JS Interop
Um in unserer Anwendung ein globales Exception Handling einbauen zu können, müssen wir die Exceptions an einer übergeordneten Stelle abfangen. Da sich eine Blazor WebAssembly SPA immer ins HTML DOM einbettet, müssen wir dies in .NET 5 (und auch in den Versionen davor) über JS Interop realisieren. Hierzu gehen wir im ersten Schritt in die index.html
unserer Blazor-WebAssembly-Anwendung und fügen folgenden Code in einem neuen <script>
-Tag hinzu.
Im obigen Code werden in zwei Fällen JavaScript Errors abgefangen und an den C#-Client-Code weitergeleitet. Eine davon ist über die console.error
-Funktion. Hier wird, sobald ein Error in die Console geschrieben wird, die Nachricht abgefangen und mit der JavaScript-Methode reportError
an den Blazor-Code weitergegeben.
Die zweite Implementierung registriert den EventListener
für das Browser Event unhandledrejection
. Hier werden alle Exceptions abgefangen, welche nicht bereits abgefangen und behandelt wurden. Wenn ein Fehler auftritt, wird dieser in unserem Code auch weiter an die JavaScript-Methode reportError
gegeben.
Abhängig von der Art des Fehlers kann die Eigenschaft reason
unterschiedlich typisiert sein. Da alle Typen jedoch von Error
ableiten, gibt es eine Eigenschaft, die immer existiert: message
. Mit reason.message
erhält man unabhängig des konkreten Fehlers immer die Error-Nachricht. Diese wird dann an die Methode reportError
weitergegeben werden.
Möchte man das ganze Objekt im .NET-Code entgegennehmen, ist zu beachten, dass auch dort jeder Typ entgegengenommen und verarbeitet werden kann. Dies beinhaltet mindestens zwei Herausforderungen die umgesetzt werden müssen:
- Es muss geprüft werden, um welchen Typ es sich bei dem übergebenen Objekt handelt.
- Und es muss geprüft werden, um welche Art von Fehler es sich handelt.
In der Funktion repeatError
wird geprüft, ob die DotNet
-API vorhanden ist (was bei Blazor WebAssembly der Fall ist). Ist dem so, wird mit der Methode invokeMethodAsync
die Exception an den Blazor WebAssembly .NET Code weitergeleitet und dort die Methode HandleJSException
aufgerufen.
Beim Aufruf von DotNet.invokeMethodAsync
ist es wichtig als ersten Parameter den Namespace zu übergeben, in welchem die Methode implementiert wurde. In unserem Beispiel ist dies Blazor.GlobalExceptionHandling
. Der zweite Parameter gibt den Namen der Methode an, welche im Blazor .NET Code als JSInvokable
implementiert wird. Dort werden wir die Exceptions dann final behandeln.
JavaScript Exception im .NET Code behandeln
Im zweiten Schritt müssen wir die Methode HandleJSException
implementieren, um die Exceptions im C#-Code abfangen zu können. Hierzu erstellen wir eine statische Methode und fügen das Attribut JSInvokable
hinzu. Als Parameter wird der Name übergeben, welchen wir im letzten Schritt im JavaScript-Methoden-Aufruf invokeMethodAsync
definiert haben.
public partial class MainLayout
{
[JSInvokable("HandleJSException")]
public static async Task HandleJSException(JavaScriptException error)
{
//Handle errors
}
}
Am Code-Beispiel sehen wir, dass die Exception als JavaScriptException
-Parameter mit der Methode übergeben wird. Wir haben hier also keine echte .NET Exception verfügbar, sondern den Fehler aus der JavaScript-Welt. So können wir nun den Fehler behandeln, um die Stabilität und Usability unserer Anwendung zu verbessern.
Das war’s auch schon!
So können mit zwei kleinen Anpassungen globale Exceptions in Blazor WebAssembly abgefangen und verarbeitet werden.
Fazit
In diesem Artikel haben wir zwei Varianten kennengelernt, mit welcher wir Exceptions in einer Blazor WebAssembly SPA abfangen und behandeln können. Im ersten Abschnitt konnten wir im .NET-Code mit Hilfe eines eigenen LoggingProvider
, Exceptions abfangen und behandeln. Hier ist es wichtig darauf zu achten, dass bei kritischen Exceptions eine hohe Wahrscheinlchkeit besteht, dass die SPA nicht mehr richtig läuft und neu geladen werden muss damit der Nutzer die Seite weiter nutzen kann. Im zweiten Abschnitt des Artikels haben wir mit cleverem Einsatz von JS Interop nicht behandelten Exceptions an den .NET-Code weitergegeben. Doch wie wir auch gesehen haben ist das Weitergeben der Exceptions nicht die Herausforderung, sondern das Verarbeiten der Exception. Dies kann je nach Implementierung im .NET Code sehr komplex werden, da das JavaScript-Objekt, je nach Exception, einen anderen Typ besitzt kann. Hier ist es wichtig, die Exception so zu verarbeiten, dass der Client weiter funktioniert und der Fehler so dokumentiert und kommuniziert wird, dass dieser schnell behoben werden kann.
Beide Varianten des Exception Handlings können in .NET 5 und natürlich auch in früheren .NET Core Version eingesetzt werden. Für .NET 6 wurde bereits ein Designvorschlag für die Umsetzung von Global Exception Handling in Blazor WebAssembly erstellt.
Der gesamte Source Code zu diesem Artikel befindet sich im zugehörigen GitHub Repository.