-2

I try to use dependency injection as often as I can but every time I end up with the same problem : I always have a giant main function.

If I use it without any framework and send the classes in the constructors, I have to initialize every class in the main and it can take quite a lot of lines.

If I use a framework with a inversion of control / a service locator the result is nearly the same because I have to initialize the classes and send them to the IOC.

Is there any way to use dependency injection without having a big giant function initializing everything ?

3
  • Somewhere you have to specify that which implementation should be used for a given interface. This can be done imperatively or declaratively. Depending on the DI framework of your choice it can support former or latter or both. Commented Oct 12, 2020 at 8:06
  • 5
    What makes you believe that injecting dependencies is some sort of special code that has to be written in line in the main function ? Simply extract that code in a module dedicated to this task. Commented Oct 12, 2020 at 8:08
  • @SteveChamaillard A lot of time, a class needs one or more other class(es) which also need(s) one or more other class(es)... Which make it hard to split the main in several submethods.
    – f222
    Commented Oct 12, 2020 at 9:24

3 Answers 3

2

How do you break up any method that's gotten too big?

By dividing it into submethods that each do a small contained subset of the whole algorithm. Whether we're talking about dependency registration or any other kind of logic is irrelevant.

In the case of dependency injection, unless there is a high degree of top-level-application decisions being made1, I tend to defer each project's DI registration to itself.

Take the following example: in my solution, I have a Web project (top-level application), a Business project (containing services) and a Data project (containing repositories). I will generally set up each project's DI inside of the projects themselves, and then call them from the top-level application.

// In Data

public static class DependencyRegistration
{
    public static IServiceCollection RegisterDataDependencies(this IServiceCollection services)
    {
        services.AddScoped<IFooRepository, FooRepository>();
        services.AddScoped<IBarRepository, BarRepository>();
        services.AddScoped<IBazRepository, BazRepository>();

        return this;
    }
}

// In Business

public static class DependencyRegistration
{
    public static IServiceCollection RegisterBusinessDependencies(this IServiceCollection services)
    {
        services.AddScoped<IFooService, FooService>();
        services.AddScoped<IBarService, BarService>();
        services.AddScoped<IBazService, BazService>();

        return this;
    }
}

// In Web Startup

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .RegisterDataDependencies();
            .RegisterBusinessDependencies();
    }
}    

The "register dependencies" name is a bit vague, but it's a byproduct of having kept the example very simple and barebones. Given a more concrete context, you can use a more concrete name, which I do advise.

Also note that while this example doesn't particularly use it, you can pass additional parameters in those methods, e.g. the connection string or any specific value you might need.


1 The reason I tend to defer this responsibility back to the projects themselves is because in most projects you encounter in the .Net ecosystem, dependency injection is not being used to give the consumer a high degree of on-the-fly (runtime) decision on which concrete implementations to use.

In most cases, it's being used to help with mocking/unit testing, and to make redevelopment/changes to the codebase easier to work with. This is hallmarked by having only one concrete implementation to every interface (excluding test projects, i.e. mocked classes).

In these cases, the top-level application doesn't really have much decision-making to do. For each interface, there's only one option, so you don't need the TLA to decide between options (since there are none). In that case, the projects themselves can just register the only concrete implementation that they've provided.

If you are dealing with a high degree of decision-making at runtime (or at least post-compilation), where there are actual choices to be made for your given interfaces, and it's more complicated than just providing alternative DI registration methods (like the example above); then it's more preferred to actually keep the DI registration logic in your top-level application.

But the same core answer remains: break your registration logic into smaller chunks.

3
  • Your IServiceCollection almost acts like a global variable. Are you sure this is dependency injection? Commented Oct 12, 2020 at 16:31
  • @user253751: that is the pattern of DI registration in ASP.NET Core. It's only used for registering objects, and not during the actual injection. Commented Oct 12, 2020 at 17:29
  • @user253751: As Bryan said, this is DI as intended in .Net Core. The collection is only a registration container though, it does not contain instances. The collection gets built into a provider, which is the object that does the actual instantiation. Also, while not a global variable you do generally only want one of them around. I don't quite see the benefit of running around with separated collections/providers. The whole point is to be able to instantiate the whole dependency graph by just requesting the top level, so you need access to all dependencies from top to bottom.
    – Flater
    Commented Oct 12, 2020 at 23:02
2

Why is that a problem? The lines of code are needed, it’s just sequential code, no benefit from splitting it up.

I don’t like a giant main() so I would have a method “injectDependencies”. Maybe if there are clearly separate areas I would split it up, but there’s no huge benefit from it.

3
  • 1
    Not sure why this question was initially down-voted. Something, somewhere, has to assemble all the pieces into a coherent picture --- and that's main. Commented Oct 12, 2020 at 12:49
  • injectDependencies is a stupid name though. call it something like create<whatever the function creates> Commented Oct 12, 2020 at 16:32
  • @GregBurghardt: Not the downvoter, but just because a large sequence of steps is needed doesn't mean that the method can't/shouldn't be broken up into smaller chunks, which is what the first paragraph implies and what I suspect a lot of people will disagree with. While the second paragraph slightly counteracts it, I'm suspecting not every downvoter who already disagrees reads on; or still disagrees with the "no huge benefit".
    – Flater
    Commented Oct 12, 2020 at 23:06
0

This is a potential use-case of auto-scanners for DI registration. I have yet to find a compelling reason to not use scanning, since any DI container (I've found) that allows scanning will also allow specific registrations to override.

For example, here is what Lamar does (https://jasperfx.github.io/lamar/documentation/ioc/registration/auto-registration-and-conventions/#sec0):

public class BasicScanning : ServiceRegistry
{
    public BasicScanning()
    {
        Scan(_ =>
        {
            // Declare which assemblies to scan
            _.Assembly("Lamar.Testing");
            _.AssemblyContainingType<IWidget>();

            // Filter types
            _.Exclude(type => type.Name.Contains("Bad"));

            // A custom registration convention
            _.Convention<MySpecialRegistrationConvention>();

            // Built in registration conventions
            _.AddAllTypesOf<IWidget>().NameBy(x => x.Name.Replace("Widget", ""));
            _.WithDefaultConventions();
        });
    }
}

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.