Search code examples
c#asp.netasp.net-core

How to prevent clients from overriding your service in C# using a public interface?


We develop a NuGet package that customer can use add to their ASP.NET site.

public interface IOurService {
  void DoStuff();
}

public class OurService: IOurService {
  public void DoStuff() {
    //do things here
  }
}

Then we have a method to register our services in DI

public static void AddOurServices(this IServiceCollection services) {
  services.AddSingleton<IOurService, OurService>();
}

The reason we have the interface IOurService is that customer wants to mock the service in their unit tests, if the service is locked down, they are saying that testing is difficult.

However, the problem is that they can override IOurService with their own implementation through DI.

services.AddOurSerivces();
services.AddSingleton<IOurService, TheirImplementation>();

Since we need the interface so the service can be mocked, we need to keep the interface and make it public. However, is there any practice to prevent clients from replacing our implementation through DI? If they do so and it does not work or introduce bugs, they will create support cases and we have to deal with that. It would be nice if we can prevent them from breaking the library in the first place.


Solution

  • I also do not understand where the problem lies, let me break it down to smaller considerations:

    You use the interface and its implementation internally

    So, you have places where you use IOurService and injecting something else might break things (although such practice is used for "extension points, like in ASP.NET).

    To solve that, you should then stick to what you had - use concrete type instead to prevent "random injections of custom implementations". Or, if you want to keep interface, create another one internal interface IOurServiceInternal : IOurService, and register it and use it instead in your library.

    The consumer use the interface in their own code (quite obvious :P )

    Then the consumer speicfically writes in their code IOurService and expects it to work out of the box. This is only accomplished when you use your extension method to register the interface AddOurServices.

    But, if the customer implements IOurService interface AND also overrides DI registration - IMO this is extremely intentional and the consumer REALLY WANTS to inject his own implementation (although, this would make using your library unreasonable). So I would not worry much about this case as well.

    So, in turn, I would not overthink this and what you have is good.

    Augmenting registartion of implentation of the interface

    The other solution is to augment how you register yor service, so it resolves to OurService:

    Considering such setup

    public interface IOurServiceLibraryConsumer
    {
        void DoStuff();
    }
    
    public class OurServiceLibraryConsumer : IOurServiceLibraryConsumer
    {
        private readonly IOurService _ourService;
    
        public OurServiceLibraryConsumer(IOurService ourService)
        {
            _ourService = ourService;
        }
    
        public void DoStuff()
        {
            //do things here
        }
    }
    

    you could register your dependencies like:

    builder.Services.AddScoped<IOurService, OurService>();
    // Just to test
    builder.Services.AddScoped<IOurService, ClientOurService>();
    builder.Services.AddScoped<IOurServiceLibraryConsumer, OurServiceLibraryConsumer>(sp =>
    {
        var ourServices = sp.GetRequiredService<IEnumerable<IOurService>>();
        var ourSerivce = ourServices.Cast<OurService>().First();
        return new OurServiceLibraryConsumer(ourSerivce);
    });