-
Notifications
You must be signed in to change notification settings - Fork 382
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
Simplified usage of BindingContext resolved command handlers #1358
base: main
Are you sure you want to change the base?
Conversation
I added 2f2796b following the suggestion from @leonardochaia |
Awesome! It looks like I would be able to remove my DI workaround with this changes. This allows to override injected dependencies for testing purposes! Can we please add this last test case? :) var parser = new CommandLineBuilder(command)
.ConfigureBindingContext(context => context.AddService<IGreeter, GreeterImpl>())
.ConfigureBindingContext(context => context.AddService<IGreeter>(sp=> new GreeterMock())) // would use Moq or something here
.Build();
|
@leonardochaia I don't understand what you mean? The first one will be done by this PR, the second one you could always do already? |
Sorry, should have been clearer. I'm just trying to be sure that the second That's why I would assert that
|
The second one thows??? Really? What exception do you get? 🤔 |
I'm sorry (again). Didn't mean to say it is throwing right now. My point is that, if the expected API is that multiple calls to So that's why I propose we add a test that represents the actual use case of overriding an already registered service. Something like EDIT: I'm not saying we should test the |
This looks awesome! 🎉 |
The new |
@@ -282,6 +282,10 @@ public static class CommandHandler | |||
Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, Task<int>> action) => | |||
HandlerDescriptor.FromDelegate(action).GetCommandHandler(); | |||
|
|||
public static ICommandHandler FromBindingContext<THandler>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be helpful for the naming to reflect that the actual handler will be created during binding. One not great idea to open up discussion: DuringBindingCreateHandlerOfType<T>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CreateLazyBoundHandler
(to match with the existing Create
)?
}) | ||
.ConfigureBindingContext(context => context.AddService(_ => action)) | ||
.ConfigureBindingContext(context => context.AddService<IBindingContextCommandHandlerInterface, BindingContextCommandHandler1>()) | ||
.ConfigureBindingContext(context => context.AddService<IBindingContextCommandHandlerInterface, BindingContextCommandHandler2>()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One concern I have with this approach is that all handler registrations for all subcommands will happen regardless of which subcommand was used, when only one subcommand is ever used. This can be a bit wasteful and is part of why we've generally steered away from the common pattern for longer-lived apps where DI is configured up front but the cost is amortized over a longer lifetime. This is very often not the case for command-line apps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jonsequitur In this case (using the lazily bound registration) only the registration is added, no instance of BindingContextCommandHandler1
nor BindingContextCommandHandler2
is actually created at this point. And neither does the call to CommandHandler.FromBindingContext
. The actual instance is first created when the command handler for the choses command is invoked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood. At invocation time though, all of these registrations happen on the BindingContext
, and coupled with the addition of non-lazy AddService
calls, this encourages the pattern I mentioned above.
Yeah, I can see that. Basically, all service registrations are technically Transient services (they all have a factory registration that is called each time the service is resolved), but of course the factory for the basic services returns singleton instances. However, the no-func overloads that I added really are Transient. Maybe add the |
I'd prefer to avoid increasing the scope of the existing DI implementation at all, which originally lacked a public A safer approach might be to allow people to bring their own |
@jonsequitur Hmm, okay, so we could just remove all the overloads to |
The way we create command handlers is undergoing some rethinking so that we can eventually remove all of the name-based matching and a lot of model binding complexity. If you're interested in the source generator approach we're looking at, take a look at this PR: #1362. |
This PR adds convenience helper methods to simplify the usage of services resolved by the BindingContext.
FromBindingContext
toCommandHandler
.Creates an
ICommandHandler
where the specified command handler type is filled in at runtime by the BindingContext. This requires that the BindingContext knows how to instantiate the handler type (requires service registration).ConfigureBindingContext
middleware.Middleware simply takes an
Action<BindingContext>
parameter. In this callback the developer can callAddModelBinder
andAddService
. This simplifies service-registration on the BindingContext.AddService
instance method onBindingContext
.Simplified service registration for trivial types. Uses a
ModelBinder
to auto-construct the service.inspired by discussion in #1344.
/cc @leonardochaia