Search code examples
c#.netexceptionmauiserilog

How can I use Serilog to catch Unhandled Exceptions in the App.cs of a .NET MAUI application?


I'm using Azure Table Storage to as a storage mechanism. I'm able to write exceptions within a ViewModel to the Azure Table from within the ViewModel, but when I try to add a handler to unhandled exceptions on App.cs the exception is logged on the Debug output window, but it isn't stored on the Azure Table.

This is my Program.cs:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                fonts.AddFont("SF-Pro-Text-Bold.otf", "SFProTextBold");
                fonts.AddFont("SF-Pro-Text-Regular.otf", "SFProTextRegular");
                fonts.AddFont("materialdesignicons-webfont.ttf", "MaterialDesignIcons");
            });

        builder.UseMauiApp<App>()
            .UseMauiCommunityToolkit()
            .RegisterDataServices()
            .RegisterViewModels()
            .RegisterViews();

        builder.Logging.AddSerilog();

        var storageConnectionString = "MY_CONNECTION_STRING";
        var tableName = "logs";

        Log.Logger = new LoggerConfiguration()
            .WriteTo.AzureTableStorage(connectionString: storageConnectionString, storageTableName: tableName)
            .WriteTo.Debug()
            .CreateLogger();

        return builder.Build();
    }
}

This is my App.cs:

public partial class App : Application
    {
        private readonly ILogger<App>_logger;

        public App(ILogger<App> logger)
        {
            _logger = logger;
            InitializeComponent();
            AppDomain.CurrentDomain.UnhandledException += HandleUnhandledException;
            MainPage = new AppShell();
        }

        private void HandleUnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            var exception = (Exception)e.ExceptionObject;
            _logger.LogCritical(exception, exception.Message);
        }
    }
}

This is one of my ViewModels where I'm implementing logging:

public class OnboardingViewModel : BaseViewModel
{
    private ILogger<OnboardingViewModel> _logger;

    public OnboardingViewModel(ILogger<OnboardingViewModel> logger)
    {
        _logger = logger;
    }

    private void OnSkipOnboardingCommand()
    {
        try
        {
            throw new Exception("Oops! Something went wrong.");
        }
        catch (Exception e)
        {
            _logger.LogError(e, e.Message);
        }
    }
}

When I try this the exception is shown on the output window and saved to the Azure Table:

enter image description here

However if I change the OnSkipOnboardCommand to this:

private void OnSkipOnboardingCommand()
{
    throw new Exception("Oops! Unhandled Exception.");
}

I receive the exception on the HandleUnhandledException methond on App.cs. The exception is logged only to the Output Window:

[0:] [07:38:43 FTL] Oops! Unhandled Exception.
System.Exception: Oops! Unhandled Exception.
   at MyApp.Mobile.ViewModels.OnboardingViewModel.OnSkipOnboardingCommand() in C:\Users\Developer\Development\MyApp\src\MyApp.Mobile\ViewModels\OnboardingViewModel.cs:line 74
   at Microsoft.Maui.Controls.Command.<>c__DisplayClass4_0.<.ctor>b__0(Object o) in D:\a\_work\1\s\src\Controls\src\Core\Command.cs:line 80
   at Microsoft.Maui.Controls.Command.Execute(Object parameter) in D:\a\_work\1\s\src\Controls\src\Core\Command.cs:line 123
   at Microsoft.Maui.Controls.TapGestureRecognizer.SendTapped(View sender, Func`2 getPosition) in D:\a\_work\1\s\src\Controls\src\Core\TapGestureRecognizer.cs:line 59
   at Microsoft.Maui.Controls.Platform.TapGestureHandler.OnTap(Int32 count, MotionEvent e) in D:\a\_work\1\s\src\Controls\src\Core\Platform\Android\TapGestureHandler.cs:line 69
   at Microsoft.Maui.Controls.Platform.InnerGestureListener.Android.Views.GestureDetector.IOnGestureListener.OnSingleTapUp(MotionEvent e) in D:\a\_work\1\s\src\Controls\src\Core\Platform\Android\InnerGestureListener.cs:line 157
   at Android.Views.GestureDetector.IOnGestureListenerInvoker.n_OnSingleTapUp_Landroid_view_MotionEvent_(IntPtr jnienv, IntPtr native__this, IntPtr native_e) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-33/mcw/Android.Views.GestureDetector.cs:line 711
   at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_Z(_JniMarshal_PPL_Z callback, IntPtr jnienv, IntPtr klazz, IntPtr p0) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:line 137
   at Java.Interop.JniEnvironment.InstanceMethods.CallNonvirtualBooleanMethod(JniObjectReference instance, JniObjectReference type, JniMethodInfo method, JniArgumentValue* args) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/obj/Release/net7.0/JniEnvironment.g.cs:line 11969
   at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualBooleanMethod(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs:line 164
   at Android.Views.GestureDetector.OnTouchEvent(MotionEvent ev) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-33/mcw/Android.Views.GestureDetector.cs:line 1639
   at Microsoft.Maui.Controls.Platform.TapAndPanGestureDetector.OnTouchEvent(MotionEvent ev) in D:\a\_work\1\s\src\Controls\src\Core\Platform\Android\TapAndPanGestureDetector.cs:line 36
   at Microsoft.Maui.Controls.Platform.GesturePlatformManager.OnTouchEvent(MotionEvent e) in D:\a\_work\1\s\src\Controls\src\Core\Platform\GestureManager\GesturePlatformManager.Android.cs:line 83
   at Microsoft.Maui.Controls.Platform.GesturePlatformManager.OnPlatformViewTouched(Object sender, TouchEventArgs e) in D:\a\_work\1\s\src\Controls\src\Core\Platform\GestureManager\GesturePlatformManager.Android.cs:line 204
   at Android.Views.View.IOnTouchListenerImplementor.OnTouch(View v, MotionEvent e) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-33/mcw/Android.Views.View.cs:line 4196
   at Android.Views.View.IOnTouchListenerInvoker.n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_(IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-33/mcw/Android.Views.View.cs:line 4137
   at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPLL_Z(_JniMarshal_PPLL_Z callback, IntPtr jnienv, IntPtr klazz, IntPtr p0, IntPtr p1) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:line 236

However the exception is not persisted on Azure Storage. This only happens with the exceptions on the HandleUnhandledException method on the App.cs.

The fact the the exception is written to the Output Window but not to the Azure Table really confuses me.

Any idea on why this is happening or how I could get Unhandled Exceptions and store them on Azure Tables using Serilog? Should I change something on my code?

What I found


Solution

  • I think there's a few options here:

    • The exception is being caught before it can bubble up to the app domain's UnhandledException event handler. I'm not familiar with how MAIU handles its logging of errors to the console, but perhaps this could be the issue.
    • In your CreateMauiApp method, where you're setting up your app and registering the view model, view, etc, it's possible that the OnSkipOnboardingCommand is being invoked before the HandleUnhandledException handler has been wired up to the app domain's UnhandledException event. You can test this by wiring up the UnhandledException event handler at the start of the CreateMauiApp method.
    • As already pointed out by a commenter on your post, it's possible that your process doesn't have enough time to write the log to the Azure table before it's been killed by the OS. You could test to see if this is the issue, by changing the ExitApplication property to false on the UnhandledExceptionEventArgs object, which will allow it to remain running.

    I'd also recommend reading Microsoft's documentation for the UnhandledException event, from memory you won't be able to catch certain classes of critical exceptions with it.