1

In a C# console application of mine (which uses CliFx for the command line processing), I'm setting up dependency injection using Autofac. What I've learned about IoC is that you should have a single composition root with all of your types known and registered there.

I'll be honest, the idea of having a single location aware of every type in the application has always bothered me. It seems at odds with several core principles I've learned to respect over the years. Specifically:

  • Separation of Concerns: Normally completely unrelated types are now together in the same place, even if they can never be used together (mutually exclusive).
  • Open-Closed Principle: In a console application designed vertically, adding a new command should be a matter of adding new files, not modifying existing ones. However, in addition to new files being added, I now have to go modify the application entry point code to add new type registrations, even if nothing other than the new classes will use those types.
  • Monolithic code: As the application grows, this function could become hundreds, maybe thousands of lines long with not just simple type mappings, but actual business rules when the registered service is actually a lambda (factory method logic used to yield an implementation to a registered interface).

I'm going to use a cheesy example here, but hopefully this gets my point across. Let's say I have a command called human. It has two subcommands, eat and sleep. Furthermore, if I use the eat command, I can choose what the human eats:

Eat Cookies and Cake:

$ human eat --sweets cookies cake

Eat Vegetables:

$ human eat --veggies carrots peas

Human can even eat a combination:

$ human eat --veggies carrots --sweets cake

Before I get into code examples, I want to state that I've greatly simplified the code required to actually work with CliFx. Please consider this pseudocode. There's a lot of details related to getting proper code for this library, but I've omitted it here to simplify things. Also this code probably doesn't compile since I just typed it up in notepad.

The way this is implemented (in my mind) is as follows. I first have my Program class, which sets up CLI processing:

internal static class Program
{
    public static async Task<int> Main() =>
        await new CliApplicationBuilder()
            .AddCommandsFromThisAssembly()
            .Build().RunAsync();
}

Every subcommand class inherits from a base command class which handles common stuff such as:

  • Common command line options available to every subcommand (such as --debug to control the level of logging output. Every subcommand logs stuff)
  • Setting up the composition root (or rather, the shared subset of it)
  • Initializing program configuration

The base command is something like this:

public abstract class BaseCommand : ICommand
{
    [CommandOption]
    public bool Preview { get; set; } = false;

    [CommandOption]
    public bool Debug { get; set; } = false;

    [CommandOption]
    public string? Config { get; set; };

    protected IContainer Container { get; set; }

    public virtual void Execute()
    {
        // Setup composition root. The Debug value is used to control the
        // desired log output level. That is set up when the logger is registered
        // in the DI container.
        Container = CompositionRoot.Setup(Debug);

        // Load configuration file & set up the singleton instance registered
        // with the DI container.
        var config = Container.Resolve<IConfigurationLoader>();
        config.Load(Config);
    }
}

Since we're running the eat subcommand, that gets its own EatCommand class:

class EatCommand : BaseCommand
{
    [CommandOption]
    public List<string> Sweets { get; set; } = new()

    [CommandOption]
    public List<string> Veggies { get; set; } = new()

    public override void Execute()
    {
        // Do the common setup stuff
        base.Execute();

        // Eat-specific registrations. Will never be used by other subcommands
        using (var scope = Container.BeginLifetimeScope(b =>
            {
                b.RegisterType<FarmersMarket>().As<IFarmersMarket>();
                b.RegisterType<Bakery>().As<IBakery>();
                b.RegisterType<EatSweetsLogic>();
                b.RegisterType<EatVeggiesLogic>();
            }))
        {
            if (Sweets.Count > 0)
            {
                var eatLogic = scope.Resolve<EatSweetsLogic>();
                eatLogic.Process(this);
            }

            if (Veggies.Count > 0)
            {
                var eatLogic = scope.Resolve<EatVeggiesLogic>();
                eatLogic.Process(this);
            }
        }
    }
}

A couple of points:

  • When we eat veggies, we obtain them from a farmer's market. IFarmersMarket is a dependency of EatVeggiesLogic. Sweets will never need this. And importantly, subcommands like SleepCommand (for command human sleep) will never need it. Same goes for sweets + bakery.
  • The "Logic" classes are just ways to decompose chunks of functionality in the subcommand. I'm not sure if it's accurate to call this a strategy pattern, but it feels similar.

Each subcommand likely has several registrations like this. Ones that are only relevant to that subcommand. So is it wrong to model the "composition root" around the command hierarchy? I think of it this way: Each subcommand is a program entry point. Basically the same as having multiple Program.Main() methods. At least, I think CliFx and most other CLI processing libraries force this semantic style.

To strictly keep things "in one place" as normal text-book definition of Composition Root seems to require means that all of these "implementation detail" classes like EatSweetsLogic need to get exposed at a higher level. This means, probably among other things, that if I remove the ability for human to eat sweets, I'm not just deleting source files. I'm going back to my CompositionRoot.cs and updating a method body. Violation of Open-Closed principle, IMHO, and I have a discomfort with this.

Lastly, I will say that I am probably mixing a few different problems here. I'm perfectly happy to keep the focus on the composition root aspect of this. Another issue I've struggled with is separating the model (the subcommand class and its properties that relate to CLI options) from execution business logic. However, this is an entirely different problem that gets more into the specifics of CliFx itself. If I've mixed issues here, please disregard. My mind is a bit jumbled since there's so much to take in.

Sorry for the long run-down, but I wanted to do a brain dump and share all of my discomforts. I'm stuck in analysis paralysis here and am not sure why this seemingly simple concept leaves me stumped and indecisive.

2 Answers 2

1

IoC in CliFx

So it seems that the actual problem that you are trying to solve is the way CliFx locates commands via AddCommandsFromThisAssembly, which prevents normal IoC workflow because commands are (seemingly) instantiated outside of IoC.

Well, the latter is not true, because CliFx supports IoC integration, so it can instantiate all the dependencies for your commands. Excerpt from the readme.md:

public static class Program
{
    public static async Task<int> Main()
    {
        var services = new ServiceCollection();

        // Register services
        services.AddSingleton<MyService>();

        // Register commands
        services.AddTransient<MyCommand>();

        var serviceProvider = services.BuildServiceProvider();

        return await new CliApplicationBuilder()
            .AddCommandsFromThisAssembly()
            .UseTypeActivator(serviceProvider.GetService)
            .Build()
            .RunAsync();
    }
}

Adapting this integration to Autofac should be as easy as replacing serviceProvider.GetService with Container.Resolve. The next step will be to move all registrations into one place (Program.Main) and to use normal dependency injection for the commands.


(Below is the old answer, that focuses mostly on the IoC in general.)


IoC containers offer two huge boons that are valid even for simple projects, imo:

  • No need to call constructors manually. This means for example that you can swap order of the arguments in a constructor, add or remove something... and that is, done — no need for call-site refactoring, change-set is limited to 1 line.

  • Composition root provides a bird-eyes view of the entire project. When looking to fix something in an unfamiliar (too new or too old) project, IoC setup procedure allows you to quickly glance over the suspects. No need to click through a chain of constructors while trying to remember where you've been already. It's a poor man's architecture.md that never lies.


I hope that addresses your main concern, let me zoom into details now.

Separation of Concerns

There is no conflict. Aggregation root does one simple job: describing the dependency chain.

Open-Closed Principle

OCP is limited to the types and type design, while dependency rules are data. (No worries, people are often confused what OCP (and it's vague friends) are about.)

Monolithic code

IoC rules are normally defined with a code. You can structure this code in any way you like. It's not uncommon to factor out the common components (that may be repeated in child/nested scopes). More specifically, take a look at Autofac's modules.

AddCommandsFromThisAssembly()

You can do just that, it's right in the Autofac's manual. You can even automatically collect every type with a given interface or an attribute. O, the blasphemy! (Nah, it's not. Welcome to the enterprise — where things have to scale.)

Each subcommand likely has several registrations like this. Ones that are only relevant to that subcommand.

Don't bother about "over-provisioning" in IoC, it is constructors's job to take only what it needs. By the way, registered dependency is not instantiated until requested, meaning there is not much of overhead for unused registrations.

Each subcommand likely has several registrations like this.

The much easier way to do what you want is to assemble full composition with every dependency possible, and then ask Autofac to resolve the command that you want to run. Don't bother with separate composition roots.

2
  • I'm sorry but I am having a lot of difficulty understanding your answer. Maybe it's the complex terminology you're using. For example, you say "assemble full composition with every dependency possible", I do not understand this. I cannot ask Autofac to resolve the command, that is a detail inside CliFx that I have no control over. Commented Apr 4, 2021 at 15:18
  • @void.pointer right, I got confused by the focus of your question. So the actual problem is not in the IoC itself, but rather that specific workflow is conflicting with IoC setup. Can you put more emphasis on that / make it more clear in the question? I'll try to reboot my answer. Commented Apr 4, 2021 at 16:37
0

OK so, yes, you have several things here which don't fit the normal DI pattern

  1. CLI application - Runs once, does a thing and quits
  2. CliFx - designed for a CLI application with multiple "commands" essentially multiple apps put into one.
  3. Your multiple mini apps don't share their dependencies

But none of these really change anything, you still have a single Program.Main() starting point, Your App has multiple parts, CliFx supports dependency injection via construction parameters on the commands.

You can always split the registration code up IoC.RegisterForCommandX(IContainer container) but call it from main and let the container handle the injection. Don't more the registration code into your commands as that breaks the whole point of separating them out in the first place. You might as well just use new

example code:

class EatCommand : BaseCommand
{
    public EatCommand(IFarmersMarket fm, IBakery b, IEatSweetsLogic esl, IEatVeggiesLogic evl)
    {
        this.fm = fm;...//etc
    }
}

public static class Program
{
    public static async Task<int> Main()
    {
        var services = new ServiceCollection();

        // Register services
        services.AddTransient<IFarmersMarket, FamersMarket>();
        services.AddTransient<IBakery, Bakery>();
        ....etc

        var serviceProvider = services.BuildServiceProvider();

        return await new CliApplicationBuilder()
            .AddCommandsFromThisAssembly()
            .UseTypeActivator(serviceProvider.GetService)
            .Build()
            .RunAsync();
    }
}
2
  • Due to the way CliFx works, I can't put much inside my Main method. There's also a catch 22 situation. I cannot begin to set up my registrations until CLI processing has happened, because command line options affect registrations (see the --debug example). So I need to: 1) process CLI 2) registrations 3) process command logic. CliFx makes this challenging, which is why I moved my composition root into BaseCommand. Can you offer a concrete example of your alternative? Commented Apr 4, 2021 at 15:21
  • just register everything in main as per the CliFx docs
    – Ewan
    Commented Apr 5, 2021 at 10:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.