Dazinator.Extensions.DependencyInjection icon indicating copy to clipboard operation
Dazinator.Extensions.DependencyInjection copied to clipboard

Typed Services

Open dazinator opened this issue 6 years ago • 2 comments

Resolving services at runtime by name is a known anti-pattern. That is not to say it isn't a useful feature that solves certain problems. The reason it is an antipattern is that:

  • resolution of the service with a specific name can fail at runtime if the service with that name has not been registered
  • the dependencies are not visible in any api surface - and in essence are therefore "hidden" unless you read the internals of the code.
  • due to above points, there is no way to check / validate that required dependencies are present when the class is constructed - which means it can technically run in an invalid state and hit a runtime exception (back to first point above).

Consider this:

public class Foo
{
  public Foo(Func<string, AnimalService> namedServices)
 {
      var one = namedServices("APOWDJAWOJD");
      var two = namedServices("TAWD");
 }
}

From the perspective of a consumer of this class, it knows there is some dependency on factory function that returns an AnimalService but it's not obvious what name is going to be used when a service is requested, therefore how can it establish how to implement the factory function, unless it can see the internal code in Foo class to work out what names will be used:


var foo = new Foo((name)=>{  // what am I meant to do here? what names will be used? })

So this dribbles down a problem when setting up DI, because the container, cannot tell from its current registrations, whether Foo actually has all of its dependencies met - because resolution of the dependency is based on a string that isn't provided until an invocation at runtime. So the factory function could return NULL or throw an exception if a named service with the name APOWDJAWOJD hasn't been registered in advance - and the container has no way to check for that.

Enter Typed registrations..

var services = new ServiceCollection();
    services.AddTyped<AnimalService>(types=>
    {
         types.AddSingleton(); // Will resolve any type used as akey 
         types.AddSingleton<BearService, MyKey>(); when resolved using: MyKey as key, will return BearService             
        
    });

Now you can do this:

public class Foo
{
   public Foo(Func<MyKey, AnimalService> typedService)
   {
      var service = typedService(); // BearService
  }
}

public class Bar
{
   public Foo(Func<SomeOtherType, AnimalService> typedService)
   {
      var service = typedService(); // default AnimalService
  }
}

Essentially this would do away with magic strings, and instead rely on "marker types" to resolve different registrations of a service.

The thinking behind this, is that it should allow the container to verify if it has a registrations, and solves basically all of the above problems. What's the downside?

  • You have to choose or create distinct "marker" types to use as keys for registration / resolution.

dazinator avatar Mar 09 '20 18:03 dazinator

I suppose C# 9 positional records could come in handy for declaring the marker types:

public record Duck();
public record Bear();
public record Goose();

image https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9#record-types

JosXa avatar Nov 18 '20 01:11 JosXa

@JosXa I agree, they look perfect for this!

dazinator avatar Nov 18 '20 08:11 dazinator