simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.21k stars 155 forks source link

First-chance exception thrown when registering open-generic type with type constraint #984

Closed pmarflee closed 9 months ago

pmarflee commented 9 months ago

Version: 5.4.1

I have the following Command and Handler classes:

public class SaveBusinessObjectCommand<TObject> where TObject : class
{
    public SaveBusinessObjectCommand(TObject businessObject)
    {
        BusinessObject = businessObject;
    }

    public TObject BusinessObject { get; internal set; }
}

public class SaveBusinessObjectCommandHandler<TObject> : ICommandHandler<SaveBusinessObjectCommand<TObject>>
    where TObject : class 
{    
    public void Handle(SaveBusinessObjectCommand<TObject> command)
    {
        ...
    }
}

I register the handler using SimpleInjector's support for open generic types:

container.Register(typeof(ICommandHandler<>), typeof(SaveBusinessObjectCommandHandler<>), Lifestyle.Singleton);

At runtime, my application is able to resolve instances of SaveBusinessObjectCommandHandler<> without any errors. However, I've noticed that when I analyse dumps of the process that my application is running in, I see reports of First Chance Exceptions like this:

Type:     System.TypeLoadException
    Message:  GenericArguments[0], 'TCommand', on 'TMSWeb.Library.Commands.SaveBusinessObjectCommand`1[TObject]' violates the constraint of type parameter 'TObject'.
    Stack:    
[InlinedCallFrame]
[InlinedCallFrame]
DomainNeutralILStubClass.IL_STUB_PInvoke(System.RuntimeTypeHandle, IntPtr*, Int32, System.Runtime.CompilerServices.ObjectHandleOnStack)
System.RuntimeTypeHandle.Instantiate(System.Type[])
System.RuntimeType.MakeGenericType(System.Type[])
SimpleInjector.Internals.GenericTypeBuilder.BuildClosedGenericImplementationBasedOnMatchingServiceType(CandicateServiceType)
SimpleInjector.Internals.GenericTypeBuilder.BuildClosedGenericImplementation()
SimpleInjector.Internals.GenericTypeBuilder.MakeClosedImplementation(System.Type, System.Type)
SimpleInjector.Internals.GenericTypeBuilder.IsImplementationApplicableToEveryGenericType(System.Type, System.Type)
SimpleInjector.Internals.GenericRegistrationEntry+OpenGenericToInstanceProducerProvider.RegistrationAppliesToAllClosedServiceTypes(System.Type)
SimpleInjector.Internals.GenericRegistrationEntry+OpenGenericToInstanceProducerProvider..ctor(System.Type, System.Type, SimpleInjector.Lifestyle, System.Predicate`1<SimpleInjector.PredicateContext>, SimpleInjector.Container)
SimpleInjector.Internals.GenericRegistrationEntry.AddGeneric(System.Type, System.Type, SimpleInjector.Lifestyle, System.Predicate`1<SimpleInjector.PredicateContext>)
SimpleInjector.Container.Register(System.Type, System.Type, SimpleInjector.Lifestyle)
TMSWeb.Library.Ioc.TMSWebLibraryBootstrapperPackage.RegisterCommandHandlers(SimpleInjector.Container, SimpleInjector.Lifestyle)
...

These errors appear to be harmless, as my application is still able to resolve instances of the open generic type when required to do so. However, I would be interested to know if there is anything I can do when registering this type that would prevent the error from being raised in the first place.

dotnetjunkie commented 9 months ago

When it comes to building generic types, sometimes the only way to verify that a type matches certain type constraints is to try build the type. I believe that there's now some sort of CanCreateGenericType method in .NET 6 or 7, but that's of to little use of Simple Injector, which needs to be compatible with older frameworks as well. The problem, unfortunately, is that CreateGenericType will throw an exception when the type constraints don't match. In that case Simple Injector catches the exception, but now knows that the type constraints didn't match.

This wasn't a big issue with older Visual Studio versions. More recent versions of VS, however, automatically break the debugger when a first-chance exception is encounters; even if it's not your own code. This new VS behavior is quite troublesome, as it stops the developer flow and can leave you in a confused state. I myself burned many hours tracking down a bug that didn't exist because I didn't realize that I was looking at a first-chance exception. Because of this change in behavior, quite some big internal changes were made in Simple Injector v5 to prevent most first-chance exceptions from occurring. I wrote more extensively about this change here.

Unfortunately, for the time being, Simple Injector can't prevent all first-chance exceptions from happening. On the plus side, these first-chance exceptions are harmless, you can ignore them, and you can configure your IDE to suppress them.

That said, using the code you provided, I've been looking at the code base and did notice that in your case (which I consider to be a happy-path scenario) the first-chance could actually be prevented. And even better, the type constraint check can be skipped, which improves performance of the registration process.

I will push a patch release shortly that fixes this issue.

dotnetjunkie commented 9 months ago

Patch release v5.4.2 has been published to NuGet.

Thank you for raising this issue.

pmarflee commented 9 months ago

Thanks for responding to my issue so quickly. I hadn't noticed these exceptions being thrown in VS. I must have set something to suppress them. I'll update to the latest version of the package when I get a chance.