Separate initialization from execution in `CommandApp`
See original GitHub issueAs a user of the API, I’d like the ability to perform global setup and teardown in my application without utilizing base classes for my Command<>
/ AsyncCommand<>
subclasses.
Is your feature request related to a problem? Please describe.
At the moment, if I want to perform some global initialization and/or cleanup, I am required to use base classes or other obscure hacks. The problem with the base class solution is that it creates a lot of constructor boilerplate and makes the code difficult to maintain when changes are needed in that common base class, especially when that change involves modifying dependencies (parameters for DI).
Describe the solution you’d like
I’d like the logic in CommandApp.RunAsync()
to be separated such that the following is separated into distinct phases of execution controlled by the user of the API:
- Parsing of the command tree. Registration of internal types and dependencies. Finally, creation of the implementation for
ITypeResolver
. - Execution of the actual commands.
This allows me to do the following:
- Perform setup of my command line application as normal.
- Invoke a new method (or even better: Use the builder pattern) to initiate step 1 above (e.g.
CommandApp.Setup()
). - Access the
ITypeResolver
via some property (e.g.CommandApp.Resolver
) to perform DI resolution for any dependencies needed for my global initialization/cleanup. - Perform global initialization as needed in
Program.Main()
- Invoke
CommandApp.Run()
and store the result. - Perform global cleanup as needed in
Program.Main()
- Return the result of the previously executed
CommandApp.Run()
command.
Describe alternatives you’ve considered
- Abusing my implementation of
ITypeRegistrar
to invoke a callback to perform custom initialization whenITypeRegistrar.Build()
is called. - Implementing a base class shared by all command handlers that implements
Command<>
orAsyncCommand<>
.
Additional context
I am willing to implement the changes needed here. I’ve already played around with some ideas in a local branch and landed on two approaches:
- Quick & dirty: Add new methods to
CommandApp
with the goal of reducing footprint. Not the most ideal change and am not completely happy with it, but it at least gets that separation I’m aiming for. - Implement something like
CommandAppBuilder
that uses a syntax similar tonew CommandAppBuilder().WithConfiguration(...).Build()
to construct aCommandApp
and return it, which will be responsible for the first phase of initialization. The returnedCommandApp
would actually be composed in a wrapper object that only allows users to invokeRun()
/RunAsync()
on it.
The second approach is more drastic, but I think there’s a way to maintain backward compatibility in both scenarios.
The reason I’m creating an issue instead of a pull request:
- I want to discuss the change before I spend time coding it. The change is non-trivial so I need to consider the maintainers’ opinions.
- I’m uncertain about how to split the logic gracefully in
CommandExecutor.Execute()
. There’s a lot of separation of concerns issues in this method:- Initialization (registration) logic is mixed in with what appears to be processing logic.
- There’s a circular dependency between DI registration and command tree processing which further complicates separation of the two.
- There’s CLI parsing code for the version options here that probably belongs in its own
Command
subclass; however I believe cleaning this up properly requires implementing a way to add “global options” to the base command without introducing new subcommands/verbs. There are other issues here that call for this, and I imagine this code exists because of the lack of this functionality.
Happy to help invest time in this library if the developers have the time to help guide me! Thank you for reading!
Issue Analytics
- State:
- Created 10 months ago
- Comments:7 (4 by maintainers)
Top GitHub Comments
Thank you for the update! It is very promising that progress is being made that gets us closer to being able to do this. I’m still using my rather unsavory workarounds to have more control over initialization logic. I’m definitely still interested in the outcomes being shared here.
As always, let me know if I can help in any way. You’re doing great!
Hello @rcdailey, just checking in to say this has not been forgotten.
Rather the work on exposing the help writer needed to be re-implemented to better support DI in
CommandExecutor.Execute()
(see my comment here: https://github.com/spectreconsole/spectre.console/discussions/1224#discussioncomment-6476669)Once done, I think the general usage of DI, and the further potential usage of DI for other, currently internal spectre.console classes, becomes easier and cleaner to do.