4

I'm using ASP.NET Core Web API. I am having a hard time wrapping my head around instantiating a non-controller class that uses DI. There are a multitude of SO articles related to this, but none that have answered my question (as far as I can understand). These are the most popular and relevant:

Net Core Dependency Injection for Non-Controller

Dependency Injection without a Controller

ASP.NET 5 Non-Controller DI injection

My use case (a contrived example):

I have a class SpeechWriter that has a dependency on IRandomTextService:

public class SpeechWriter
{
    private readonly IRandomTextService _textService;
    
    // Constructor with Text Service DI
    public SpeechWriter(IRandomTextService textService)
    {
        _textService = textService;
    }

    public string WriteSpeech()
    {
        var speech = _textService.GetText(new Random().Next(5,50));

        return speech;
    }
}

IRandomTextService interface:

public interface IRandomTextService
{
    public string GetText(int wordCount);
}

and the implementation:

public class RandomTextService : IRandomTextService
{
    public string GetText(int wordCount)
    {
        return Lorem.Words(wordCount);
    }
}

IRandomTextService is registered as a service in Startup.cs:

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

    services.AddScoped<IRandomTextService, RandomTextService>();
}

In my controller action, if I want to instantiate a SpeechWriter like this:

public IActionResult Index()
{
    var speech = new SpeechWriter();

    return Ok(speech.WriteSpeech());
}

I can't do it because an argument (the injected service) is expected.

SpeechWriter requires the RandomTextService as a parameter

The only way I can seem to get DI to inject RandomTextService in SpeechWriter is if SpeechWriter itself is a service and injected in the controller:

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

    services.AddScoped<IRandomTextService, RandomTextService>();
    services.AddScoped<SpeechWriter>();
}
public class EchoController : ControllerBase
{
    private readonly SpeechWriter _speechWriter;
    
    public EchoController(SpeechWriter speechWriter)
    {
        _speechWriter = speechWriter;
    }
    
    
    public IActionResult Index()
    {
        return Ok(_speechWriter.WriteSpeech());
    }
}

Is there any way to get RandomTextService injected when SpeechWriter is instantiated as in my first example, like this?

var speech = new SpeechWriter();

If not, what is it about DI that I'm missing? My actual application is more complex than this and I would effectively have to create a chain of DI and services all the way back up to the controller. I could use the ServiceProvider "anti-pattern", but I prefer not to do that because I'd be passing ServiceProvider all over the place.

Please help educate me!

Thanks.

realmikep
  • 593
  • 1
  • 6
  • 22
  • 1
    "*I would effectively have to create a chain of DI and services all the way back up to the controller*" -- I don't follow this point. You inject `SpeechWriter` into whatever class needs it. How does that create a chain back to the controller? – canton7 May 12 '21 at 10:56
  • 7
    What is wrong with injecting the `SpeechWriter`? Actually the controller has a dependency on it, so it should be injected and not instantiated manually. If you see a `new YourType` in your code it looks suspicious – Tim Schmelter May 12 '21 at 10:56
  • 5
    _"a chain of DI and services all the way"_ is exactly the goal. Either you do DI or you don't. – H H May 12 '21 at 10:57
  • 5
    The mistake here is that you are `new`ing up `SpeechWriter` instead of injecting it. – DavidG May 12 '21 at 10:57
  • 4
    The point of DI is making dependencies explicit by injecting them -- explicitly. A DI *framework* is just a way of doing this by orchestrating things at the top level. When you want to create a new `SpeechWriter`, you must also have an `IRandomTextService` at hand. How you get it is up to you, but you cannot leave it out and pretend `SpeechWriter` works on its own -- that's a feature, not a bug. Note that a higher-level class can also have a class take a dependency on a factory (e.g. `Func`) to abstract over the creation without taking a hard dependency on the framework. – Jeroen Mostert May 12 '21 at 10:57
  • 1
    @pinkfloydx33 not my actual code! – realmikep May 12 '21 at 11:01
  • Why not add the missing parameter to Index? – jdweng May 12 '21 at 11:03
  • 1
    Rule of thumb: When using DI *never* write "new" – Klamsi May 12 '21 at 11:04
  • 1
    @pinkfloydx33: what bug? Note that the empty `Random`-constructor behaviour has changed with .net core. It no longer relies on the system time. The seed will always be random. – Tim Schmelter May 12 '21 at 11:05
  • 4
    All these comments are extremely helpful -- I am being educated! Based on these comments, it seems I'm not strictly adhering to DI because I'm new-ing up a class that uses DI. If I need to do that, then it's a sign that I need to do some refactoring to implement DI correctly. – realmikep May 12 '21 at 11:07
  • @TimSchmelter wow it appears you are correct. I retract my comment. Also interesting there's now a [`Random.Shared` property](https://github.com/dotnet/runtime/blob/ee32ee2240f5771f9cf162695d110c2adccc5107/src/libraries/System.Private.CoreLib/src/System/Random.cs#L51) – pinkfloydx33 May 12 '21 at 11:11
  • 1
    I'm also struggling with grasping the "practicalities" of DI, as the author was. Does that mean, that if I have registered service e.g. doing logging, db-connection or random text, then I will also need to register all consumers of these services....and in turn all consumers of these? – thomas a. h. Apr 24 '22 at 07:25

0 Answers0