New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify working with subsets of collections #517

Closed
dotnetjunkie opened this Issue Feb 23, 2018 · 7 comments

Comments

2 participants
@dotnetjunkie
Collaborator

dotnetjunkie commented Feb 23, 2018

Simple Injector has a very decent strategy when it comes to working with collections. However, when subsets of collections need to be registered to certain consumers, the solution becomes rather complicated.

Take the following example for instance where there is a collection of IZ instances and two consumers C1 and C2 that both depend on IEnumerable<IZ>, but both require a subset of IZ instances.

public interface IZ { }
public class Z1 : IZ { }
public class Z2 : IZ { }
public class Z3 : IZ { }
public class Z4 : IZ { }

public class C1
{
    public C1(IEnumerable<IZ> collection) => Collection = collection;
    public IEnumerable<IZ> Collection { get; }
}

public class C2
{
    public C2(IEnumerable<IZ> collection) => Collection = collection;
    public IEnumerable<IZ> Collection { get; }
}

With Simple Injector v4.0, this can be achieved as follows:

var container = new Container();

container.Register<C1>();
container.Register<C2>();
var z1 = Lifestyle.Transient.CreateProducer<IZ, Z1>(container);
var z2 = Lifestyle.Transient.CreateProducer<IZ, Z2>(container);
var z3 = Lifestyle.Transient.CreateProducer<IZ, Z3>(container);
var z4 = Lifestyle.Transient.CreateProducer<IZ, Z4>(container);

// Z1, Z2, Z3 injected into C1
container.RegisterConditional(typeof(IEnumerable<IZ>),
    Lifestyle.Singleton.CreateRegistration(() => ProducerIterator(z1, z2, z3), container),
    c => c.Consumer.ImplementationType == typeof(C1));

// Z2, Z3, Z4 injected into C2
container.RegisterConditional(typeof(IEnumerable<IZ>),
    Lifestyle.Singleton.CreateRegistration(() => ProducerIterator(z2, z3, z4), container),
    c => c.Consumer.ImplementationType == typeof(C2));

Here, ProducerIterator is the following extension method:

private IEnumerable<T> ProducerIterator<T>(params InstanceProducer<T>[] producers) where T : class
{
    foreach (var producer in producers) yield return producer.GetInstance();
}

@dotnetjunkie dotnetjunkie added this to the v4.1 milestone Feb 23, 2018

@dotnetjunkie

This comment has been minimized.

Collaborator

dotnetjunkie commented Mar 2, 2018

I suggest adding a Collections property to the Container instance to allow grouping of collection related methods that allow simplifying working with collections. For instance:

Collections.Create<TService>

// Collections.Create allows creating a stream based on the supplied types. Iterating the stream
// would produce instances according to their lifestyle (as usual).
IEnumerable<IZ> stream1 = container.Collections.Create<IZ>(typeof(Z1), typeof(Z2), typeof(Z3));
IEnumerable<IZ> stream2 = container.Collections.Create<IZ>(typeof(Z2), typeof(Z3), typeof(Z4));

container.RegisterSingleton(new C1(stream1));
container.RegisterSingleton(new C2(stream2));

Collections.CreateRegistration<TService>(params Type[])

container.Register<C1>();
container.Register<C2>();

container.RegisterConditional(typeof(IEnumerable<IZ>),
    container.Collections.CreateRegistration<IZ>(typeof(Z1), typeof(Z2), typeof(Z3)),
    c => c.Consumer.ImplementationType == typeof(C1));

container.RegisterConditional(typeof(IEnumerable<IZ>),
    container.Collections.CreateRegistration<IZ>(typeof(Z2), typeof(Z3), typeof(Z4)),
    c => c.Consumer.ImplementationType == typeof(C2));

Collections.CreateRegistration<TService>(params InstanceProducer<TService>[])

var z1 = Lifestyle.Transient.CreateRegistration<Z1>(container);
var z2 = Lifestyle.Transient.CreateRegistration<Z2>(container);
var z3 = Lifestyle.Transient.CreateRegistration<Z3>(container);
var z4 = Lifestyle.Transient.CreateRegistration<Z4>(container);

container.RegisterConditional(typeof(IEnumerable<IZ>),
    container.Collections.CreateRegistration<IZ>(z1, z2, z3),
    c => c.Consumer.ImplementationType == typeof(C1));

container.RegisterConditional(typeof(IEnumerable<IZ>),
    container.Collections.CreateRegistration<IZ>(z2, z3, z4),
    c => c.Consumer.ImplementationType == typeof(C2));

The Container.Collections method grouper also allow making the 'append to collection' feature more discoverable, since that feature now hidden behind the SimpleInjector.Advanced namespace:

// This
container.Collections.AppendTo(typeof(IValidator<>), typeof(DataAnnotationsValidator<>));

// instead of
using SimpleInjector.Advanced;
container.AppendToCollection(typeof(IValidator<>), typeof(DataAnnotationsValidator<>));
@TheBigRic

This comment has been minimized.

Collaborator

TheBigRic commented Mar 5, 2018

Although I do not really understand when and why somebody would need a subset of a collection I think, assuming this is a valid use case, the approach is very clean, super easy to read and therefore far more understandable then the needed syntax in 4.0.

Maybe offtopic, but:
Is there also possibility we can make it a little easier to differ from lifestyles in through this new "namespace"?

Consider this registration:

container.RegisterCollection(typeof(IValidator<>), assemblies);

Maybe I'm wrong but I think this is the equivalent of:

var types = container.GetTypesToRegister(typeof(IValidator<>), assemblies, 
                new TypesToRegisterOptions
                {
                    IncludeComposites = false
                });
var collection = new SomeSuperSmartSimpleInjectorContainerControllerCollectionClass();
foreach (var type in types)
{
    // when the configured default lifestyle == Lifestyle.Transient:
    var registration = Lifestyle.Transient.CreateRegistration(type, container); 
    collection.Add(registration);
}

container.RegisterSingleton(typeof(IEnumerable<IValidator<>>), collection);

Which makes it somewhat hard to specify the lifestyle of the items in the stream. Agreed not a feature needed often, but there are some corner cases where I need this.

@dotnetjunkie

This comment has been minimized.

Collaborator

dotnetjunkie commented Mar 6, 2018

Maybe I'm wrong but I think this is the equivalent of:

Pretty close.

Which makes it somewhat hard to specify the lifestyle of the items in the stream.

Not at all. You can change the lifestyle of individual registrations in a collection as follows:

var types = new[]
{
    typeof(OrderValidator), typeof(CustomerValidator), typeof(ProductValidator)
};

container.RegisterCollection(typeof(IValidator<>), types);

// Override lifestyles
container.Register<CustomerValidator>(Lifestyle.Singleton);
container.Register<ProductValidator>(Lifestyle.Scoped);
@TheBigRic

This comment has been minimized.

Collaborator

TheBigRic commented Mar 6, 2018

Well of course. But that is not really intuitive code if you ask me. Especially if you compare that to my code sample, where I let Simple Injector scan for all implementations and let it group this by type.

I use this extension method to do this and my question was if that was maybe something useful to add to the Collection method grouper:

private static void RegisterCollection<TService>(this Container container, 
                            IEnumerable<Assembly> assemblies, Lifestyle lifestyle)
{
    var options = new TypesToRegisterOptions
    {
        IncludeComposites = false,
        IncludeDecorators = false,
        IncludeGenericTypeDefinitions = true,
    };
    var types = container.GetTypesToRegister(typeof(TService), assemblies, options);
    container.RegisterCollection(typeof(TService),
        from type in types
        select lifestyle.CreateRegistration(type, container));
}
@dotnetjunkie

This comment has been minimized.

Collaborator

dotnetjunkie commented Mar 9, 2018

Which makes it somewhat hard to specify the lifestyle of the items in the stream.

There are several ways to influence the lifestyle of the items in the collection:

  1. You can register the concrete type using Register<T>(Lifestyle) as shown in my last comment.
  2. You can supply a list of Registration instances to RegisterCollection.
  3. You can replace the default ILifestyleSelectionBehavior.

You can supply a list of Registration instances to RegisterCollection

var types = container.GetTypesToRegister(
    typeof(IValidator<>),
    assemblies,
    new TypesToRegisterOptions { IncludeComposites = false });
  
//   DetermineLifestyle is a custom method
var registrations =
    from type in types
    let lifestyle = DetermineLifestyle(type)
    select lifestyle.CreateRegistration(type, container);
    
container.RegisterCollection(typeof(IValidator<>), registrations);

You can replace the default ILifestyleSelectionBehavior

// Here MyCustomLifestyleBehavior has the same role as DetermineLifestyle
// of the previous example, but is now used globally by the container.
container.Options.LifestyleSelectionBehavior =
    new MyCustomLifestyleBehavior(container);
    
container.RegisterCollection(typeof(IValidator<>), assemblies);
@dotnetjunkie

This comment has been minimized.

Collaborator

dotnetjunkie commented Mar 23, 2018

Perhaps the Container.RegisterCollection methods should be moved as well.

Using the new Collections.AppendTo, this will be typical codeL

container.RegisterCollection(typeof(IValidator<>), typeof(IValidator<>).Assembly);
container.Collections.AppendTo(typeof(IValidator<>), typeof(DataAnnotationsValidator<>));

Perhaps this should become:

container.Collections.Register(typeof(IValidator<>), typeof(IValidator<>).Assembly);
container.Collections.AppendTo(typeof(IValidator<>), typeof(DataAnnotationsValidator<>));

@TheBigRic, any opinion about this?

@TheBigRic

This comment has been minimized.

Collaborator

TheBigRic commented Mar 23, 2018

I have normally an opinion about everything.. :-)

Looks very clean. As a matter of fact I think this will further clarify that S.I. separates normal registrations from registration of collections. Questions regarding this still pop us regulary on the interwebs

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