63

I am trying to port an ASP.NET application to ASP.NET Core. I have property injection (using Ninject) on my UnitOfWork implementation like this:

[Inject]
public IOrderRepository OrderRepository { get; set; }
[Inject]
public ICustomerRepository CustomerRepository { get; set; }

Is there a way to achieve the same functionality using built-in DI on .NET Core? Also, is it possible to use convention-based binding?

Pang
  • 9,564
  • 146
  • 81
  • 122
Özgür Kaplan
  • 2,106
  • 2
  • 15
  • 26

5 Answers5

67

No, the built-in DI/IoC container is intentionally kept simple in both usage and features to offer a base for other DI containers to plug-in.

So there is no built-in support for: Auto-Discovery, Auto-Registrations, Decorators or Injectors, or convention based registrations. There are also no plans to add this to the built-in container yet as far as I know.

You'll have to use a third party container with property injection support.

Please note that property injection is considered bad in 98% of all scenarios, because it hides dependencies and there is no guarantee that the object will be injected when the class is created.

With constructor injection you can enforce this via constructor and check for null and the not create the instance of the class. With property injection this is impossible and during unit tests its not obvious which services/dependencies the class requires when they are not defined in the constructor, so easy to miss and get NullReferenceExceptions.

The only valid reason for Property Injection I ever found was to inject services into proxy classes generated by a third party library, i.e. WCF proxies created from an interface where you have no control about the object creation. And even there, its only for third party libraries. If you generate WCF Proxies yourself, you can easily extend the proxy class via partial class and add a new DI friendly constructor, methods or properties.

Avoid it everywhere else.

Tseng
  • 61,549
  • 15
  • 193
  • 205
  • 1
    Would you consider logger injected via property bad practice as well? I find it very reasonable – pikausp Dec 20 '18 at 13:37
  • @pikausp: Yes, Imho its bad (or rather inferior) to constructor injection. I mean either your application needs logging or not. With the new .NET Core logging its just easier, you inject `ILogger` weather or not its configured doesn't matter from point of the application and you always have the security that the object won't be `null`, so no need for null checking or null coalescing operator – Tseng Dec 20 '18 at 14:21
  • 21
    Do you instead want me to have a dozen of constructor arguments, and also spend another dozen of initialization codelines, all to avoid some "NRE" which never happens anyway (if coded right)? – AgentFire Nov 26 '19 at 18:17
  • 6
    @AgentFire: Its not about writing some extra lines (which btw. can be done 90% via refactoring helpers, i.e create, initialize and nullcheck constructor parameters), its also bad for writing unit tests (since dependencies become non-obvious, someone testing a class wont know which dependency he/she needs provide or mock if its hidden behind a property, while on constructor its insanely obvious). on top of that, constructor injection makes it obvious when a class does too much (when you have more than 4-5 dependencies, it means your abstraction is bad and class tries to do too much) – Tseng Nov 27 '19 at 01:07
  • @Tseng the obviousness issue is eliminated by using `[Inject]` attribute, which is the best way to inject properties anyway. As for what if some injection is missed, that is easily covered with tests, any decent test will throw NRE in the case. And regarding "too many dependencies", the topic is too broad, but I'd argue that it is completely possible and reasonable to have more than 5, or even 10 of them, in lots of cases. – AgentFire Nov 27 '19 at 08:26
  • 9
    @AgentFire: Nope, it's not. When you do `var sut = new MyDomainService()` it's **NOT** obvious what the dependencies are, especially not if class is located in a different library (whose source may or may not be accessible to the team consuming it). However, `var sut = new OrderService(orderRepository, customerRepository)` since you have to pass it to the constructor, you instantly see it (class cant be instantiated w/o). Also `var sut = new MyService(dep1,dep2, dep3, dep4, dep5, dep6, ... dep10)` also makes you instantly obvious, your class does to much and that you need to refactor it – Tseng Nov 27 '19 at 08:32
  • 1
    I recognize you lack the experience with developing big business critical application, where business rules needs to be implemented and where the classes have to guarantee their invariants, but with time an experience you'll come to realize it too, that constructor injection is the only clean approach to keep code simple and maintainable and find design issues at an early development stage (rather than late after go-live). The easier code is to read and understand the more productive and less error prone you are. Constructor injection just helps you do that – Tseng Nov 27 '19 at 08:34
  • 9
    @Tseng no need to get stuffy mr 'big business critical application', we all write those. I use property injection with projects in my company as a favorable approach to avoid boiler plate that's bound to change a lot. Good DI frameworks should do the nullchecks for you when configured properly. Only thing you need to adapt to is to not create service instances explicitly with constructors. It takes less time to learn that than writing a constructor with 5 arguments. – user3608068 Jul 01 '20 at 11:43
  • 2
    @user3608068: No, you and the other posters don't get it. Things will happen and it won't be injected. Or when you mock a service for unit test, impossible to know which services are needed and which not. with property inject you need to look at the implementation and if the service is needed and for which calls in your service so you can mock and assign it. For constructor injection, you clearly see it on the constructors signature. No need to search or debugging why your unit test failed with a null reference exception. It saves 10 or 100 thousands of EUR while developing – Tseng Jul 01 '20 at 17:26
  • Property injection is just bad style, opposite of clean code and clean architecture and shall only be used where absolutely unavoidable (like in my example above: In auto generated proxy classes which are instantiated outside of your control such as WCF services - which lucky are deprecated these days) – Tseng Jul 01 '20 at 17:28
  • 10
    @Tseng Ctor injection requires you to repeat every name four times. This is just ideology. Every 10 years or so one of those fads rolls over the industry and forces everyone to type pointless boilerplate so that some egghead can feel important until tooling catches up and hides the clutter away again. Last time that was setters and getters in Java where something that should have been on a line was inflated to half a screenful. – John Jan 17 '21 at 12:46
  • 1
    @Tseng Should C# get required property setters you're losing the only feeble argument you have in this case and this plague ends. – John Jan 17 '21 at 12:47
  • 1
    @John What boilerplate are you talking about? You still writing your code in vim? When I inherit a class, I type in `public class`, add the `: BaseClass` for inheritance and then hit `Ctrl`+`.` to get the "Generate Constructor for 'MyClass(..., ...)' " takes two whole seconds and can guarantee the classes invariants (Don't know what it is? Google it). Guaranteed class invariants is a major metric in maintainable code that is less error prone and an very important measurement of quality code. – Tseng Jan 17 '21 at 23:35
  • 1
    Want write code thats unmaintainable which not even you can read in a couple of months? Sure, do it the way you want, but don't complain how error prone your code is or when it blows up at production time, cause an dependency is missing and that uncaught null exception blows up your software and blocks your business, costing you 6-figure until its fixed. – Tseng Jan 17 '21 at 23:37
  • 1
    Everyone here arguing with @Tseng, give it 5 years of experience or so and, unfortunately, you'll all have learnt the hard way he's right. – CodeAngry Mar 18 '21 at 04:37
  • Regardless of whether DI is involved or not, if something is required for a class to do its job it should be passed in the constructor. To do anything else is risking errors and confusion.That may change if C# ever gets a "required property" flag of some form, but for now that's just the way it is. – Boschy Mar 19 '21 at 02:56
  • I also don't see "required properties" happening as there's a whole can of worms around dealing with the time between construction of a class and the required properties being set. – Boschy Mar 19 '21 at 03:02
  • 1
    There is at least a proposal: https://github.com/dotnet/csharplang/blob/main/proposals/required-members.md. And the related issue: https://github.com/dotnet/csharplang/issues/3630 – berhir Jul 14 '21 at 15:45
  • sorry, in Java world we inject by @Autowired annotations, and I never heard any about null of it? How it can be null if your project is configured correctly? – Nhân Trần Mar 16 '22 at 01:45
  • you gave part of the answer your self. apps are not static and can change and thus the configuration, i.e. removing a registration but not the property. second newing the class won't do anything which is what you do in unit tests, where you may want to mock the deps – Tseng Mar 19 '22 at 03:57
  • With the addition of init-only properties in C# 9.0 the discussion can done anew; I mean a DI service could use reflection over those `init` properties and inject them, and they would be immutable after object construction - esentially bypassing the long constructor argument list, but still asserting that those properties are passed/injected during object creation. – Dynalon Jul 08 '22 at 05:46
  • 1
    @Dynalon: Not really, the main arguments that it hides dependencies remains. When doing unit tests, having constructor dependencies makes it dead obvious which dependencies you need. Properties don't, even if they are init-only/get-only – Tseng Jul 09 '22 at 18:30
  • 1
    And [`required` keyword](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required#code-try-0) is added as of C# 11. I'm in favor of constructor injection because it means less contract by not having public setters, but I'm looking forward to try `required` if it means less boilerplate :) – snipsnipsnip Dec 28 '22 at 02:48
  • @Tseng But when using `required` (C# 11), you also get compile-time hints during development which properties are needed and what your dependencies are. So no "hidden" dependencies anymore. – Dynalon Jun 23 '23 at 07:28
  • @Dynalon: Trick question: What is C#'s `required` + `init` when the code is compiled to IL? If you don't know it, google it, then reread the answer and you'll see its still correct, 7 years after being asked and before any of that features you mention existed. Discussion over. – Tseng Jun 24 '23 at 11:30
  • Spoiler: `required init` isn't property injection – Tseng Jun 24 '23 at 11:37
  • @Tseng: Not going for trick questions or personal/"I know it better" approaches here. I am genuinly interested in arguments against/for property-based injection using latest C# features. IL doesn't play a role in my argument that "hidden dependencies" are no longer the case, when you make all injected properties `required`. – Dynalon Jun 24 '23 at 11:47
21

Is there a way to achieve the same functionality using built-in DI on .NET Core?

No, but here is how you can create your own [inject] attributes with the help of Autofac's property injection mechanism.

First create your own InjectAttribute:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class InjectAttribute : Attribute
{
  public InjectAttribute() : base() { }
}

Then create your own InjectPropertySelector that uses reflection to check for properties marked with [inject]:

public class InjectPropertySelector : DefaultPropertySelector
{
  public InjectPropertySelector(bool preserveSetValues) : base(preserveSetValues)
  { }

  public override bool InjectProperty(PropertyInfo propertyInfo, object instance)
  {
    var attr = propertyInfo.GetCustomAttribute<InjectAttribute>(inherit: true);
    return attr != null && propertyInfo.CanWrite
            && (!PreserveSetValues
            || (propertyInfo.CanRead && propertyInfo.GetValue(instance, null) == null));
  }
}

Then use your selector in your ConfigureServices where you wire up your AutofacServiceProvider:

public class Startup
{
  public IServiceProvider ConfigureServices(IServiceCollection services)
  {
    var builder = new ContainerBuilder();
    builder.Populate(services);
    
    // use your property selector to discover the properties marked with [inject]
    builder.RegisterType<MyServiceX>().PropertiesAutowired((new InjectablePropertySelector(true)););

    this.ApplicationContainer = builder.Build();
    return new AutofacServiceProvider(this.ApplicationContainer);
  }
}

Finally in your service you can now use [inject]:

public class MyServiceX 
{
    [Inject]
    public IOrderRepository OrderRepository { get; set; }
    [Inject]
    public ICustomerRepository CustomerRepository { get; set; }
}

You surely can take this solution even further, e.g. by using an attribute for specifying your service's lifecycle above your service's class definition...

[Injectable(LifetimeScope.SingleInstance)]
public class IOrderRepository

...and then checking for this attribute when configuring your services via Autofac. But this would go beyond the scope of this answer.

Pang
  • 9,564
  • 146
  • 81
  • 122
Felix K.
  • 14,171
  • 9
  • 58
  • 72
7

The top voted answer is not quite correct. While the built-in DI container (IServiceProvider) is intentionally kept simple and feature-light, it is also quite extensible, and it is possible to provide the functionality you require with a bit of plumbing.

This is what the Quickwire NuGet package provides out of the box. This works as you would expect.

First decorate the class with [RegisterService], and the properties to be injected using the [InjectService] attribute:

[RegisterService(ServiceLifetime.Scoped)]
public class BusinessLogic
{
    [InjectService]
    public IOrderRepository OrderRepository { get; set; }
    [InjectService]
    public ICustomerRepository CustomerRepository { get; set; }

    // ...
}

And then use ScanCurrentAssembly from ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.ScanCurrentAssembly();

    // Register other services...
}

This will scan the current assembly for classes decorated with RegisterService and register them in the IServiceCollection, also ensuring dependencies are injected in setters decorated with [InjectService].

Once again, this approach does not require you to use a third-party dependency injection container like Autofac. All your existing code will continue to work without any modification.

Flavien
  • 7,497
  • 10
  • 45
  • 52
2

You can use this nuget package, that extends standard Microsoft Dependency Injection and adds property injection:

https://www.nuget.org/packages/DJMJ.Extensions.DependencyInjection.Property/1.1.0

Mark property for injection

using Microsoft.Extensions.DependencyInjection;

public class FooService : IFooService
{
    [Inject]
    public IBooService BooService { get; set; }

    public void Foo()
    {
        // just start using injected property
        BooService...
    }
}

Add services scan method in ConfigureServices

using Microsoft.Extensions.DependencyInjection;

...

 host.ConfigureServices((services)=>
            {               
                services.AddTransient<IBooService, BooService>();
                services.AddTransient<IFooService, FooService>();

                // scan method
                services.AddPropertyInjectedServices();
            });

If you using this extension in asp net and want add property injection support in controllers too, you should add in ConfigureServices this statement:

 services.AddControllers().AddControllersAsServices()
David
  • 21
  • 1
  • 2
1

I don't want to argue because I'm new to .NET Core. But imagine such a situation: class A uses the services Service1, ..., Service5 and their interfaces IService1, ..., IService5 are passed in its constructor as dependencies; class B inherits from class A, class C from class B, etc. and all these derived classes should therefore pass at least the IService1, ..., IService5 interfaces in their constructors. However, class B and all classes derived from it are placed in consumer libraries or applications and are therefore beyond the reach of the class A developer.

Well, now the developer of class A will find that some functionality incorporated into class A would deserve refactoring and outsourcing into an external service, because it needs to be used elsewhere, out of reach of class A. So it outsources it to Service6 with interface IService6. But by adding another argument to the class A constructor, the developer violates the contract with users of this class: The constructor is used in derived classes and knows only its 5 arguments.

Solutions? Alternatively:

  1. Inject IService6 by a property.

  2. Instead passing 5 interfaces of individual services to the constructor, pass a single interface, which will provide 5 interfaces to the above mentioned 5 services in its properties. This interface will be part of the package shipped with class A. If any class A functionality needs to be outsourced to a service, another property will be added to this interface and another service will be added to the bundled package. All class A dependencies (perhaps except options) will be clearly summarized in this interface.

  3. Is there any better solution?

Jan Jurníček
  • 126
  • 1
  • 5
  • 1
    In the case of a distributed library, presumably there is some kind of semantic version control in place. If the devs want to introduce a new dependency without breaking things, then they would overload the constructor of Class A with a new constructor that includes IService6, and then they would include code to maintain backwards compatibility with the "old" way which doesn't require Service6. Otherwise, presumably this would be a breaking change and would likely be a well documented major version update. – jwatts1980 Sep 29 '22 at 20:56