1

I'm writing a .NET 6 application for which users can create plugins. In some situations however, when a plugin throws an unhandled exception, my own application crashes as well. That should not happen, no matter what. The plugin may stop working, it may unload, it may whatever, just leave the parent app alive. Loading happens like this:

public static ServiceInfo? LoadService(string relativePath)
{
    var loadContext = new ServiceLoadContext(relativePath);
    _alcs.Add(loadContext);
    try
    {
        var assembly = loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(relativePath)));
        var shouldLoadDll = false;
        foreach (var type in assembly.GetTypes())
        {
            if (typeof(IMonitorableService).IsAssignableFrom(type))
            {
                var directoryName = new FileInfo(relativePath).Directory!.FullName;
                if (Activator.CreateInstance(type, new object[] { directoryName }) is IMonitorableService result)
                {
                    shouldLoadDll = true;
                    return new ServiceInfo
                    {
                        Category = Directory.GetParent(relativePath)!.Name,
                        Name = Path.GetFileNameWithoutExtension(relativePath),
                        AssemblyPath = relativePath,
                        Service = result!
                    };
                }
            }
        }
        if (!shouldLoadDll)
        {
            loadContext.Unload();
        }
    }
    catch (Exception)
    {
        // This is handled, but this won't catch the exception in the plugin
    }
    
    return null;
}

I have my share of try/catch phrases, and since these IMonitorableServices are BackgroundServices, they're started like

public async Task StartAsync(CancellationToken cancellationToken)
{
    foreach (var service in _options.Services)
    {
        try
        {
            await service.Service.StartAsync(cancellationToken);
        }
        catch (Exception ex)
        {
            // This is handled, but it won't catch the exception in the plugin
        }
    }
}

Now I doubt that it's really relevant to provide the specific error, but just in case: it's a

'System.InvalidOperationException: 'Collection was modified; enumeration operation may not execute',

following an operation on event subscriptions. I know how to solve that in the plugin, but I could never trust my future plugin writers to always handle their exceptions (or prevent them from happening). I need some way to catch absolutely everything in my own application. I've been breaking my head over this and I can find many considerations on plugins loaded in AppDomains, but they're from the .NET Framework era...

Who has an idea how to solve this? I could hardly imagine this is something that has been overlooked in .NET Core/6 development.

Update: I find that other type of exceptions actually are caught within the StartAsync method. So it might have something to do with the exception being raised from an event in the plugin (don't want to put you on the wrong track though). I must add, the event is registered from within the StartAsync method, but it seems to bypass the regular catch.

Mavara
  • 11
  • 3
  • You might be having issue because you are using `await` inside `foreach`. Try using `for` instead. Refer this link https://stackoverflow.com/questions/41561365/running-async-foreach-loop-c-sharp-async-await, https://stackoverflow.com/questions/30260858/async-await-using-linq-foreach – Karan Aug 03 '22 at 09:53
  • Thanks a lot for you suggestion. I tried it, but no luck there. – Mavara Aug 03 '22 at 10:02
  • _"when a plugin throws an unhandled exception, my own application crashes as well. That should not happen, no matter what."_ <-- **This is impossible** (this is simply how OS processes _fundamentally work_): The only way to have a plugin system that cannot crash the host application is by hosting plugins and other untrusted code out-of-process (i.e. in a worker process) which means you'll need IPC. – Dai Aug 03 '22 at 10:16
  • I bet $5 that it's a `TypeInitializationException` being thrown shortly after your plugin is loaded but before your `StartAsync` is invoked - likely some `static` field has a buggy initializer or some `class` has a buggy `static` constructor. Change your Debugger settings to disable "Just my code" and to break on _all_ exceptions (not just unhandled/uncaught exceptions). – Dai Aug 03 '22 at 10:20
  • 1
    The exception you are reporting may be cause by changes (add, remove) to the `_options.Services` collection causing the foreach enumeration to stop. In fact, the foreach is outside your try...catch block. – Alberto Aug 03 '22 at 10:56
  • @Dai: thanks for commenting. I tried that, but I know where the exception occurs. Not close to invoking StartAsync, but on raising an event. There's no TypeInitializationException anywhere (I'll send you my bank account number for those $5). It may be true that plugin exceptions always directly impact the host application, the fact that some exceptions *will* be caught, shows that there can be possibilities to handle those exceptions from the host. – Mavara Aug 03 '22 at 11:28
  • @Alberto: the exception is thrown by the plugin. It's true that a collection has been changed, but it's not this one. It's probably true that this exception from an event isn't caught by this try/catch, but then the question is: how to catch that one from my host app (I should consider my plugin writers to be as prone to failure as I am ;) ). – Mavara Aug 03 '22 at 11:31
  • 1
    @Mavara _"the fact that some exceptions will be caught, shows that there can be possibilities to handle those exceptions from the host"_ - yes, **but** you cannot do anything if the plugin causes a `StackOverflowException` or `ThreadAbortException` or any of the other _very easy to throw_ but **uncatchable** exceptions - don't forget `Environment.FailFast()`. Oh, and don't forget that since .NET Core replaced .NET Framework there's less API/ABI stability between .NET versions, so that's another reason to go with the out-of-proc (i.e. IPC) route instead. – Dai Aug 03 '22 at 11:38
  • @Mavara "but I know where the exception occurs" - so do you want our help with fixing it... or not? If you're _only_ asking about how to prevent plugins from crashing your host application (which is impossible, as I've already explained) then you should remove all irrelevant details about the original exception. – Dai Aug 03 '22 at 11:39
  • @Mavara I'll be happy to donate to the charity of your choice, just send a link (but only after you post a screenshot of your Debugger's Output window first) – Dai Aug 03 '22 at 11:40

0 Answers0