Inject Differently: Property Injection in .NET Web APIs

Constructor injection has served .NET developers well—but sometimes it helps to step a little outside the norm. This post demonstrates a lightweight way to extend the built-in DI container for .NET, giving you more flexibility when you need it.

Inject Differently: Property Injection in .NET Web APIs
Photo by Sara Bakhshi

Why did I do this?

A reasonable question. Constructor injection in .NET has been the standard for a while, with good reason. It's explicit, reliable, and well-integrated with the built-in DI container. But developers have been crying out for a cleaner form of injection in .NET since... well, pretty much forever.

And in .NET 8, we finally got... something. Kinda. .NET 8 introduced "Primary Constructors", a new way to declare constructors for a class, but also a new way to do dependency injection (sort of). The syntax looks like this:

public class MyController(MyService service)

You declare the constructor at the same time as the class. There's less boilerplate, and no need to manually assign fields.

So why am I not enamoured with them?

The key problem is that primary constructor parameters are just that. Parameters. They're not readonly, and they're not surfaced as members. You can use them anywhere in the class, but only because .NET captures them in closures under the hood. It works, but it's not quite the injection pattern many of us were hoping for.

But what other options are there?

I'm personally a fan of how Angular handles this. Originally, you'd inject services via the constructor and get a private property at the same time:

constructor(private service: MyService) { }

I think this sort of syntax was what a lot of .NET devs, me included, were hoping we would get with primary constructors, and it's certainly similar, but as mentioned previously, the fundamental behaviour is very different.

Nowadays, with Angular's "renaissance" and the move towards more functional patterns, injection doesn't even require a constructor:

private service = inject(MyService);

Through clever use of JavaScript symbols, it is now possible in Angular to inject, with a function, directly into the property itself.

And that got me thinking: Could I add something like that to .NET? A clean, easy to use syntax that "just works"? Well, as it turns out...

Infuse

Here's the basic idea:

    public static T Infuse<T>(
      this T obj, 
      IServiceProvider provider
    ) where T : notnull
    {
        foreach (PropertyInfo property in obj.GetType()
            .GetProperties(
              BindingFlags.Public | 
              BindingFlags.Instance | 
              BindingFlags.DeclaredOnly)
            .Where(p => p.CanWrite
                && (p.DeclaringType?.GetField(
                  $"<{p.Name}>k__BackingField", 
                  BindingFlags.Instance | BindingFlags.NonPublic)
                  ?.IsInitOnly ?? false)
                && Attribute.IsDefined(p, typeof(RequiredMemberAttribute))))
        {
            object service = provider.GetRequiredService(property.PropertyType);
            property.SetValue(obj, service);
            service.InjectProperties(provider);
        }

        return obj;
    }

This extension method takes any object and a service provider instance, and then:

  • Loops over the object's properties
  • Selects the members that match the convention
  • Attempts to resolve the service from the provider
  • Injects the service by setting the property
  • Runs recursively to inject dependencies into dependencies

This is a simplified version for demonstration purposes. In the full version, there are various configurable options to cater to different preferred code styles and use cases. But with the above method, the end result is that you can declare a public, required property with an init-only setter on any service or controller:

public required IService Service { get; init;}

And the system automatically injects the corresponding implementation from the service collection into it.

The Convention

Why those specific modifiers? Well, the reason for it being public is that, while you can assign to a private field via reflection, you're generally not supposed to, because it can lead to unintended side effects.

As for why it's required, when you declare a property like this without actually assigning to it, either inline or in the constructor, the compiler gives you a warning. Not only does the "required" keyword remove this warning, because you're telling the compiler it will be assigned before it's used, the more modifiers we add, the less likely it is to clash with any other property or field. We look for this keyword by checking if the lowered IL has placed the "RequiredMember" attribute on the property.

Lastly, the init-only setter is there to ensure immutability, for the same reasons you might put "readonly" on a private field used for constructor DI. In fact, you might notice that we're checking "IsInitOnly" on the backing field rather than the property, and that's because the lowered IL actually adds the "readonly" modifier to the backing field of the auto-property if you specify an init-only setter. That's literally the only difference between { get; set; } and { get; init; }, but again, it gives us an extra thing to look for that makes it less likely to clash with any existing convention.

Activation

This is all well and good, magic properties and all that, but how do we actually call the "Infuse" function?

ASP.NET Core has a well-defined pipeline for handling HTTP requests. Each step in that pipeline does a specific thing, and then passes the result onto the next step. In our case, we're interested in the IControllerActivator. This takes information extracted from the route handler previously, which tells the pipeline which controller and endpoint the request should be handled by, and basically does the following:

    public object Create(ControllerContext context)
    {
        return ActivatorUtilities.CreateInstance(
          context.HttpContext.RequestServices,
          context.ActionDescriptor.ControllerTypeInfo.AsType()
        );
    }

The activator utility does a very similar thing to what we're doing in the Infuse extension method, just with constructor parameters instead of class members. But it loops over them, attempts to retrieve the corresponding implementation using the service provider, assigns to the parameter, and runs recursively to inject each parameter's dependencies.

By writing our own controller activator, we can take this object, and simply add in our extension method:

    public object Create(ControllerContext context)
    {
      object obj = ActivatorUtilities.CreateInstance(
        context.HttpContext.RequestServices,
        context.ActionDescriptor.ControllerTypeInfo.AsType()
      );
    
      obj.Infuse(context.HttpContext.RequestServices, Options);
    
      return obj;
    }

I hope you can see from this that our system is simply extended existing behaviour, rather than replacing anything. That means we can actually use our property injection alongside constructor injection, and even utilise both in the same class if we really want to.

Registration

But how does it know to use our controller activator instead of the default one? Well, the .NET team thought of that when designing the pipeline. You might notice that when I spoke about the activator initially, I referred to it as an interface (IControllerActivator). That's because it's actually registered in the DI container as a transient service that can be injected into the pipeline itself. Yes, they use the DI container to implement the DI container.

But what that means for us is that we can simply write another extension method for the service collection that swaps out the default implementation for our custom one, and on startup, call that extension:

    public static IServiceCollection AddInfuse(this IServiceCollection services) 
    {
      return services.Replace(
        ServiceDescriptor.Transient<IControllerActivator, 
        InfuseActivator>()
      );
    }
builder.Services.AddInfuse();

Add this line to program.cs

Et voilà. You now have a system that can handle property injection in .NET with, I think, a reasonable convention, that allows you in many cases to not have any kind of constructor at all in your service classes.

Of course, you can extend this (and I have) to include a bunch of different options, such as:

  • Strict Mode: Basically, throw a custom error if something goes wrong instead of failing silently and giving out unhelpful NullReferenceExceptions.
  • Customisable Convention: Change the type of member, accessibility modifier, or add a custom predicate to determine which class members can be injected.
  • Attributes: Require a specific attribute to be used to be more explicit, and don't rely on convention. Or, use it alongside the convention for even more granular control.
  • Max Recursion: Add a maximum recursion depth so that it doesn't get stuck in infinite loops, and/or a predicate so that it is only conditionally recursive.
  • Error Handling: Add a callback of what to do if a service hasn't been registered.
public bool StrictMode { get; set; } = false;
public InfuseMethods InfuseMethods { get; set; } = InfuseMethods.Convention;
public InfuseMembers Members { get; set; } = InfuseMembers.Properties;
public InfuseAccessibilityModifiers AccessibilityModifiers { get; set; } = InfuseAccessibilityModifiers.Public;
public int MaxRecursionDepth { get; set; } = -1;
public Func<MemberInfo, InfuseOptions, bool> MemberInjectionPredicate { get; set; } = DefaultMemberInjectionPredicate;
public Func<MemberInfo, InfuseOptions, bool> RecursiveInjectionPredicate { get; set; } = DefaultRecursiveInjectionPredicate;
public Action<Type, MemberInfo, InfuseOptions> OnServiceNotFoundHandler { get; set; } = DefaultServiceNotFoundHandler;
public List<Type> InjectionAttributes { get; set; } = [typeof(InjectAttribute)];

An example of extended "Infuse" options

The Downsides

It's all well and good having a property injection setup that feels like magic, but nothing comes for free, so what are the potential pitfalls?

  1. Without the aforementioned "Strict Mode", you don't get as clear and immediate errors as you would with constructor injection, but that's largely because it's not a .NET feature, and so doesn't get that "first-class citizen" treatment. If .NET added this kind of thing as an option in the future, I'm sure the error handling would be much more explicit. But in the meantime, a "Strict Mode" can help with that.
  2. Reflection always brings overhead. To be honest, in this case, I think the performance difference between constructor and property injection is almost negligible, and every property you inject is a constructor parameter not injected, so it kind of balances out. That said, it would be remiss of me not to mention that it is, technically, in the order of microseconds, slower.
    1. Source generation could help with this, but that's a whole other blog post.
  3. Testing is more complicated. You can still mock dependencies, but you can't just instantiate the object with the mocked services in the same way as with constructor injection, so you'd need to explicitly set each property with the correct service. You could automate that process, it's definitely a solvable problem, but it is a problem introduced by this system that the constructor approach just does not have to contend with, so I think it's worth mentioning.

Inject Differently

I suspect the majority of devs will probably be happy to stick with constructor injection. It's still plenty convenient, it's familiar, and it's an established, robust system that's been refined over many years.

But I also think that every now and then, we need to look at the status quo and ask: "Could this be better?"