In diesem Artikel

Ein bei Entwicklern vielleicht nicht immer so beliebtes, aber für den Endbenutzer enorm wichtiges Thema, ist das Abfangen und Behandeln von Fehlern. Selbst wenn eine Anwendung vor der Bereitstellung gründlich getestet wurde ist es immer möglich, dass der Benutzer auf (unerwartete) Fehler stößt. In diesem Artikel schauen wir uns anhand eines Beispiels an, wie wir Fehler in Blazor WebAssembly an zentraler Stelle abfangen und verarbeiten können.

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.

Blazor WebAssembly - Global exception handling

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<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
        Func<TState, Exception, string> 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>(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<ILoggerProvider, CriticalExceptionLoggingProvider>(services =>
{
    var navigationManager = services.GetService<NavigationManager>();
    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.

<script>
  function reportError(error, level, destination) {
      if (DotNet) {
        DotNet.invokeMethodAsync('Blazor.GlobalExceptionHandling', 'HandleJSException', {
            errorMessage: error,
            logLevel: level,
            logDestination: destination
        });
      } else {
        console.log('DotNet is not available');
      }
    }

  console.error = function (msg) {
      reportError(msg, 'Critical', 'Console');
  }

  window.onunhandledrejection = event => {
    reportError(event.reason.message, 'Error', 'JavaScript-Code');
  };
};
</script>

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.

Invoke .NET Method from JavaScript

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.

Verpassen Sie keine weiteren Artikel, Webinare und Screencasts unserer Experten und melden Sie sich zu unserem kostenlosen monatlichen Newsletter an.

Related Articles

 | Christian Weyer

Blazor WebAssembly 5 und die Kopplung an .NET 5 - guter Ausblick für die Zukunft Blazor WebAssembly 5 ist im November 2020 als Teil von .NET 5 sechs Monate nach dem Erscheinen der ersten offiziellen Version (3.2.0) released worden und profitiert an diversen Stellen von…

Read article
 | Pawel Gerr

In general, you can divide template engines into two types. The relatively simple ones are using template strings with placeholders to be replaced by some concrete values. The other template engines can do everything the simple ones can but additionally provide means for control…

Read article
 | Patrick Jahr

Seit der Version Blazor WebAssembly 3.2.0 enthält Blazor umfangreiche Unterstützung für clientseitige Authentifizierung, wodurch die Implementierung von OpenID Connect und OAuth2 in Single-Page-Applications (SPAs) deutlich vereinfacht wird. In diesem Artikel sehen wir uns an, wie…

Read article