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.22k stars 152 forks source link

Enable fluent API #374

Open dotnetjunkie opened 7 years ago

dotnetjunkie commented 7 years ago

All Register overloads of the registration API return void. By instead returning an object that describes the made registration, it would allow users and third parties to build extension methods on top of this.

Here are some examples of what can be done on top of this:

// InitializeWith method that forwards to container.RegisterInitializer<TImplementation>()
container.Register<IService, ServiceImpl>().InitializeWith(s => s.Value = configValue);

// Register a configuration value
container.Register<IService, ServiceImpl>().WithConstructorArgument(configurationString);
dotnetjunkie commented 4 years ago

There are many ways to design a fluent API, but it all comes down to returning objects from registration methods that have some similarity, because this allows a single extension method to be reused on top of multiple Registration methods.

There are two philosophies that I can think of:

Either way, designing a API that is usable is difficult. We need good use cases, while they might be scarce. Even the previously presented examples are not easily implemented using such API. A WithConstructorArgument extension method would currently only work (even with a fluent API) when the user override the default dependency injection behavior (IDependencyInjectionBehavior), because Register prevent registrations for implementations that contain constructor arguments of ambiguous types.

Another issue is that those extension methods should allow users to tune that specific registration or set of registrations that they made for that single call to Register, but for the most part, Simple Injector works more globally. For instance:

container.Collection.Append<ILogger, DiskLogger>().WithConstructorArgument("c:\\log.txt");
container.Collection.Append<ILogger, DiskLogger>().WithConstructorArgument("d:\\log.txt");

Here the user would expect that DiskLogger is registered twice, but both with a different string constructor argument. Both calls to Register<ILogger, DiskLogger>, however, reuse the same Registration instance, which means that both loggers will log to "d:\log.txt". Oops.

This same problem could appear for InitializeWith or for instance an DecoratorWith extension method:

Type[] handlerTypes1 = ...
Type[] handlerTypes2 = ...

container.RegisterDecorator(typeof(ICommandHandler<>), typeof(LoggingDecorator<>));

container.Register(typeof(ICommandHandler<>), handlerTypes1)
    .DecorateWith(typeof(ValidationDecorator<>)
    .DecorateWith(typeof(PerformanceMonitoring<>);

container.Register(typeof(ICommandHandler<>), handlerTypes2)
    .DecorateWith(typeof(DeduplicationDecorator<>);

container.RegisterDecorator(typeof(ICommandHandler<>), typeof(SecurityDecorator<>));

In the example above, the user would expect the handlers of list 1 to be decorated with: logging, validation, performance, and security (in that order), but the handlers of list 2 to be decorated with: logging, de-duplication, and security.

Although a nice feature to have, these extension methods can't simply be put on top of Simple Injector without other changes.

In the end, I'm starting to think adding a fluent API is less and less attractive, because:

Because of this I'm moving this feature out of the v5 milestone. Moving it back to the backlog.

CasperWSchmidt commented 4 years ago

How about the simple step of returning the container instead of void? This would allow a series of regitrations:

container.Register(typeof(ICommandHandler<>))
    .RegisterDecorator(typeof(ICommandHandler<>), typeof(LoggingDecorator<>))
    .RegisterDecorator(typeof(ICommandHandler<>), typeof(SecurityDecorator<>));

Instead of having each registration on its own line prefixed with container.

dotnetjunkie commented 4 years ago

@CasperWSchmidt, I would argue against this. The added benefit of not having to type container is IMO limited. But, more importantly, after Simple Injector starts returning Container from its registration methods (say in v6), it becomes almost impossible to extend this further with a Fluent API as discussed above. Or, at least, not without introducing big breaking changes that would cause 90% of the users to have to make changes to their Composition Root.

So I rather wait until we found a compelling way to implement a fluent API, than returning Container and been stuck with this till kingdom comes.

CasperWSchmidt commented 4 years ago

@dotnetjunkie That's alright. Perhaps I misread the monologue/discussion above, but as I understood it, you don't see any good ways to implement a fluent API. That's why I suggested the small change that would make most use cases easier but have no effect on more complex use cases (that would still require special handling and custom implementation of IDependencyInjectionBehavior).