Skip to content
This repository was archived by the owner on Nov 2, 2018. It is now read-only.

Adding support for constrained open generics#635

Closed
jbogard wants to merge 11 commits intoaspnet:masterfrom
jbogard:ConstrainedOpenGenerics
Closed

Adding support for constrained open generics#635
jbogard wants to merge 11 commits intoaspnet:masterfrom
jbogard:ConstrainedOpenGenerics

Conversation

@jbogard
Copy link

@jbogard jbogard commented Apr 9, 2018

(again)

See #471 for details. Figured I'd give this another shot.

The general scenario is:

Suppose I have some interface, IRepository<T>. This interface allows me to save an entity to a database, perhaps wrapping EFCore DbContext. As part of saving, I want to validate so I have IValidator<T>.

The implementation DbContextRepository<T> uses an enumerable of IValidator<T> to validate:

class DbContextRepository<T> {
    public DbContextRepository(
        DbContext db, 
        IEnumerable<IValidator<T>> validators) {
        // etc
    }
}

I want to be able to support:

class IFooValidator<T> : IValidator<T> where T : IFoo { }

instead of

class IFooValidator : IValidator<IFoo> {}

For the reasons in #471.

In case anything has changed, just wanted to throw this out there. I modified the approach to do what Autofac does, which is actually check the constraints if they exist (instead of the previous try-catch-swallow approach).


return genericType == parameter;
}
catch (Exception)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick - this is supposed to catch line#295 right? Specifically for catch-alls, it would be clearer to make the try{} body as specific as possible, so perhaps catch in 296?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call!

@Eilon
Copy link
Contributor

Eilon commented Apr 10, 2018

@muratg / @davidfowl - can you take a look?

@jbogard
Copy link
Author

jbogard commented Apr 10, 2018

For posterity of "how do other containers handle this?":

SimpleInjector is the odd one out here but @dotnetjunkie would know more I suppose about the approach he took.

Unity is the only ASP.NET DI-supporting container I could find that does not support this feature. I'm not sure the impact, that package has 2k NuGet downloads and Autofac has >1M.

var specialConstraints = argumentDefinitionTypeInfo.GenericParameterAttributes;

if ((specialConstraints & GenericParameterAttributes.DefaultConstructorConstraint)
!= GenericParameterAttributes.None)
Copy link

@IDisposable IDisposable Apr 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the preferred pattern vs an equality check?

if ((specialConstraints & GenericParameterAttributes.DefaultConstructorConstraint)
                    == GenericParameterAttributes.DefaultConstructorConstraint)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's a flag enum, I guess it's either that or Enum.HasFlag (which is really slow.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♂️

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to add some tests for these cases, is that better in the DI.Tests project?


private static bool IsGenericTypeDefinedBy(Type type, Type openGeneric)
{
return !type.GetTypeInfo().ContainsGenericParameters
Copy link

@IDisposable IDisposable Apr 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRYer

{
    var typeInfo = type.GetTypeInfo();
    return !typeInfo.ContainsGenericParameters
                       && typeInfo.IsGenericType
                       && type.GetGenericTypeDefinition() == openGeneric;
}

{
genericType = typeDefinition.MakeGenericType(genericArguments);
}
catch (Exception)
Copy link

@IDisposable IDisposable Apr 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we catch only the expected exceptions? From the documentation, that's

InvalidOperationException | The current type does not represent a generic type definition. That is, IsGenericTypeDefinition returns false.

So we could/should be testing typeDefinition.IsGenericTypeDefinition first.

ArgumentNullException | typeArguments is null.-or-Any element of typeArguments is null.

This is demonstrably not true as we've already dereferenced that at line 287.

ArgumentException | The number of elements in typeArguments is not the same as the number of type parameters in the current generic type definition.-or-Any element of typeArguments does not satisfy the constraints specified for the corresponding type parameter of the current generic type.-or-typeArguments contains an element that is a pointer type (Type.IsPointer returns true), a by-ref type (Type.IsByRef returns true), or Void.

This one we have checked the length matches already, but the rest of the criteria could have been violated. Thus we do need to catch (ArgumentException)

NotSupportedException | The invoked method is not supported in the base class. Derived classes must provide an implementation.

This one might be thrown, not sure we can prevent that. Either someone can request a deeper dive or we can make a case that we do need to catch (NotSupportedException) too.

@dadhi
Copy link
Contributor

dadhi commented Apr 11, 2018

What about this case?

public interface I<T> {}
public interface X<A, B> where A : I<B> {}
public class Y<A> : X<A, String> where A : I<String> {}

collection.AddTransient(typeof(X<,>), typeof(Y<>);

Copy link

@IDisposable IDisposable left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good now.

One final thought. Do we want to do any introspection or white-listing of the DI containers to know which ones will support this constraining and either throw or just return true; from the IsCompatibleWithGenericParameterConstraints method when the DI doesn't work with these constraints?

@jbogard
Copy link
Author

jbogard commented Apr 11, 2018

Would existing containers even be affected? If they didn't support it before, they blew up with exceptions. Unity for example just blows up with that ArgumentException. They'd only see the blow up during service resolution, not configuration, so for those containers that don't support filtering services based on generic constraints, they'd continue to blow up today.

From my understanding of the extensions, they either replace IServiceCollection, or dump its contents into their own container configuration. So AFAIK, containers that supply their own IServiceProviderEngine won't ever hit the code in this PR. Unity will continue to break :)

@IDisposable
Copy link

Perfect then... just wasn't wrapping my head around the full use case.

@pakrym
Copy link
Contributor

pakrym commented Apr 11, 2018

Would existing containers even be affected? If they didn't support it before, they blew up with exceptions. Uni

It doesn't exactly work like that.

As soon as a feature is added to DI.Specification AspNetCore might take a dependency on it and would start exploding with containers that used to run fine on.

@jbogard did you validate that all mentioned containers actually pass new spec test?

@ENikS what are your thought on this feature? Do you think it's worth adding? Would you be willing/able to support it in unity?

@pakrym
Copy link
Contributor

pakrym commented Apr 11, 2018

How does this change affect time to first service benchmark (https://github.com/aspnet/DependencyInjection/blob/dev/benchmarks/DI.Performance/TimeToFirstServiceBenchmark.cs)?

@jbogard
Copy link
Author

jbogard commented Apr 12, 2018

What's the implication of moving it out of the DI.Specification? I just want the feature in this container, I'm less concerned if every container does. Though I wasn't sure if features are added to this container just for ASP.NET Core, or other libraries that also want to use it.

I'll update with benchmark #'s.

I've also opened an issue with Unity here: unitycontainer/container#81

I can also open a PR with them.

@jbogard
Copy link
Author

jbogard commented Apr 12, 2018

@ENikS
Copy link
Member

ENikS commented Apr 12, 2018

I would like to implement this feature but I am not sure what is correct behavior. Is there a concise description of how it should behave?

Correct me if I am wrong. I am assuming it applies to either resolving collection or single service. When resolving collection it should return only these that satisfy constraint and when resolving single service it should blow?

@jbogard
Copy link
Author

jbogard commented Apr 12, 2018

@ENikS it mainly concerns collection behavior. If I'm resolving IEnumerable<IService<...>> then only the IService implementations that can close the generic parameters should be returned. Then based on the generic parameters and what fits constraints I may have a different list of services returned.

It really isn't for single services, but most containers I've checked just let it apply to both, and let the parent service's needs dictate the behavior (if I require a service, and none exists, blow up).

The real-world scenarios are in cases where I still need the generic parameter for a further dependency. I want the generic parameter to "flow" down, instead of terminating with a base type. Keeping IFooService<T> : IService<T> where T: Foo instead of subclassing to IFooService : IService<Foo>. If the implementer then has dependencies, it's lost the T parameter to then get other dependencies that may depend on that T parameter.

In terms of what it means to support this behavior, all containers just put a try-catch around closing an open generic type (some try to catch the simple cases without just catch-first) since the methods to actually test a closed type aren't public in the BCL. See the links above for how other containers do it. It can be pretty simple to support, just as simple as try-catch-don't match.

@ENikS
Copy link
Member

ENikS commented Apr 12, 2018

I will add this to next patch release of Unity.

@dadhi
Copy link
Contributor

dadhi commented Apr 12, 2018

Correction regarding DryIoc.

It does the filtering when registering by walking down the constraints.

The try-catch in the link above is for resolution phase to wrap the unlikely failed match in container exception.

@jbogard
Copy link
Author

jbogard commented Apr 12, 2018 via email

@ENikS
Copy link
Member

ENikS commented Apr 19, 2018

As of v5.8.6 Unity supports constrained generics

@issafram
Copy link

@jbogard Wanted to make sure to ask this. If you are adding support for generics, my assumption is that the following example would work out using the Lazy<T> class:

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

public class TestClass
{
    private readonly Lazy<ILogger> lazyLogger;
    private ILogger Logger => this.lazyLogger.Value;

    public SomeClass(Lazy<ILogger> logger)
    {
        this.lazyLogger = logger;
    }

    public void SomeMethod()
    {
        var logMessage = "Test Message to log";
        this.Logger.Log(logMessage);
    }
}

And I can just register my type as below:
.AddTransient<ILogger, ConsoleLogger>()

I apologize as I haven't read the entire conversation.
I was about to start work on a PR for that use case.
Let me know if my assumption is correct or not.
Thanks.

@ENikS
Copy link
Member

ENikS commented Apr 23, 2018

Not sure who you asking but in Unity it is supported scenario.

@issafram
Copy link

@ENikS I was actually asking @jbogard but that doesn't matter.

I have used Unity in all of my ASP.NET apps. So I am familiar with it.
I use Lazy as much as possible for objects that may or may not be used in a service. No need to resolve if it isn't used in an instance.

I was trying to go with the new built-in container included in ASP.NET Core for any new work.
That is when I recognized that my use case wouldn't work.

But if sticking to Unity is the only viable solution, then I will stick with it using the Unity.Microsoft.DependencyInjection package.

@khellang
Copy link
Contributor

Resolving Lazy<T> out of the box won't work, no. You'd have to register it using something like services.AddTransient<Lazy<ILogger>>(p => new Lazy<ILogger>(() => p.GetService<ILogger>())); 😕

@davidfowl
Copy link
Member

@muratg lets put this into the 2.2 milestone for consideration.

@muratg muratg added this to the 2.2.0 milestone May 29, 2018
@natemcmaster natemcmaster changed the base branch from dev to master July 2, 2018 17:57
@jbogard
Copy link
Author

jbogard commented Aug 20, 2018

@muratg So we're in the 2.2 window now...thoughts?

@jbogard
Copy link
Author

jbogard commented Oct 2, 2018

bump

@muratg
Copy link

muratg commented Oct 2, 2018

@davidfowl @pakrym please take a look.

@muratg
Copy link

muratg commented Oct 17, 2018

@davidfowl could you either merge or close this? @natemcmaster is going to move this to the mondo repo and moving PRs are tricky. Thanks.

@jbogard
Copy link
Author

jbogard commented Oct 17, 2018 via email

@pakrym
Copy link
Contributor

pakrym commented Oct 17, 2018

We would like to get this feature in for 3.0 but in a bit different shape (I managed to convince @davidfowl to catch first chance exceptions). It means, unfortunately, this PR would have to be closed and reopened against new repo.

@jbogard
Copy link
Author

jbogard commented Oct 18, 2018 via email

@muratg
Copy link

muratg commented Oct 18, 2018

Thanks @jbogard!

@muratg muratg closed this Oct 18, 2018
@davidfowl
Copy link
Member

first chance exceptions are the 👿. We should do what we can in ns2.0 and ask for new APIs in corefx to make it possible to avoid throwing.

@jbogard
Copy link
Author

jbogard commented Oct 18, 2018

@davidfowl I agree but this is what every container developer has had to deal with. May be easier now to expose the APIs now.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.