8

I'm just starting with Asp.net core Dependency Injection, and my concept could be inaccurate. This docs.asp.net post explains how to inject context to a controller. I have little confusion regarding injection, in testing perspective. Assume we have following scenario:

public interface ITasksRepository
{ 
   public void Create();
}

//This is fake implementation, using fake DbContext for testing purpose
public class TasksRepositoryFake : ITasksRepository
{
   public void Create()
   {
     FakeDbContext.Add(sometask);
     //logic;
   }
}

//This is actual implementation, using actual DbContext
public class TasksRepository : ITasksRepository
{
   public void Create()
   {
     DbContext.Add(someTask);
     //logic;
   }
}

Now in order to inject context in controller, we design it as:

public class TasksController : Controller
{
    public ITasksRepository TaskItems { get; set; }

    public TodoController(ITaskRepository taskItems)
    {
        TaskItems = taskItems;
    }
    //other logic
 }

What asp.net core provides as builtin feature is, we can register the dependency injection in startup class as follows:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc();
    services.AddSingleton<ITasksRepository, TasksRepositoryFake>();
}

According to this logic, my TaskRepositoryFake will be injected to the controller. So far, everything is clear. My questions/confusions regarding this are as follows:

Questions:

  • How can I use this builtin DI feature to inject the context using some logic? May be programatically, or configuration based, or environment based? (for example, always inject fake context, when using 'test' environment? etc.)
  • Is it even possible? If we always have to change this manually in StartUp class, then how does this builtin DI feature serve us? Because we could have simply done that in controller, without this feature.

2 Answers 2

7

First to answer your question: Yes, you can inject the dependency programmatically. By using factories, as is always the case with injecting dependencies based on run-time values. The AddSingleton has an overload which takes an implementationfactory and so a basic example for your use case looks like:

   public class Startup
{
    public bool IsTesting { get; }

    public Startup(IHostingEnvironment env)
    {
        IsTesting = env.EnvironmentName == "Testing";
    }

    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<ISomeRepository>(sp => IsTesting ? (ISomeRepository)new SomeRepository() : (ISomeRepository) new FakesomeRepository());
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, ISomeRepository someRepository)
    {
        app.UseIISPlatformHandler();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync($"Hello World from {nameof(someRepository)}!");
        });
    }

    // Entry point for the application.
    public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}

The concerning line of code for your TasksRepository would look like:

services.AddSingleton<ITaskRepository>(sp => isTesting?(ITasksRepository)new TasksRepositoryFake(): (ITasksRespository)new TasksRepository() );

Even better would be to put it in a factory (again with my example):

services.AddSingleton<ISomeRepository>(sp => SomeRepositoryFactory.CreatSomeRepository(IsTesting));

I hope you see how this helps you setting it up config based, environment based, or however you want. I you are interested I wrote more about DI based on run-time values via abstract factories here.

Having said that, with unit tests I would simply inject my fakes in the classes that are under test. Unit tests are there to still prove to yourself and your co-workers that the code still does as intended. And with integration tests I would make a special StartUp class with all my fakes and give it to the test host as ASP.NET Core allows you to do. You can read more about the test host here: https://docs.asp.net/en/latest/testing/integration-testing.html

Hope this helps.

Update Added cast to interface because the ternary conditional has no way of telling. Plus added some basic samples.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks Danny. I'm trying to implement your suggestions, but it is giving me a weird error. (probably it is not in your logic, but at my side.) once i'm successful, I'll mark the answer. and the error is: ..There is no implicit conversion between TaskRepositoryFake and TaskRepository. I have exactly the same implementation as i mentioned in question. I'm looking into it at my end, meanwhile can you confirm once again, if it allows this in this specific scenario.
Well, I now notice that ITaskRepository is a class. Did you mean interface? Other than that it seems you didn't exactly copy the code above. Because in my line of code it would never try to convert a TaskRepository to a TaskRepositoryFake (or vice versa). So please check it carefully.
It's a typo here. ITaskRepository is actually an interface. Otherwise I would be getting compile time errors, as class cannot have only methods signature, but definition as well. I am exactly using the logic you provided, but still above error :( .. Technically, there shouldn't be such error, as TaskRepository and TaskRepositoryFake, both implement ITaskRepository interface.
I appologize. With ternary conditions you need to explicitly cast it. See update. I also provided a more complete basic sample for the use case. You should be good to go now.
2

You can inject your dependencies configuration based, or environment based, or both.

Option 1 : Environment Based

    public IHostingEnvironment env{ get; set; }
    public Startup(IHostingEnvironment env)
    {
        this.env = env;
    } 
    public void ConfigureServices(IServiceCollection services)
    {
        if (env.IsDevelopment())
        {
            // register other fake dependencies
            services.AddSingleton<ITasksRepository, TasksRepositoryFake>();
        }
        else
        {
            // register other real dependencies
            services.AddSingleton<ITasksRepository, TasksRepository>();
        }
    }

Option 2 : Configuration Based

    public IConfigurationRoot Configuration { get; set; }
    public Startup()
    {
       var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
        .AddEnvironmentVariables();
       Configuration = builder.Build();
    } 

    public void ConfigureServices(IServiceCollection services)
    {
        var isFakeMode= Configuration["ServiceRegistrationMode"] == "Fake";
        if (isFakeMode)
        {
            // register other fake dependencies
            services.AddSingleton<ITasksRepository, TasksRepositoryFake>();
        }
        else
        {
            // register other real dependencies
            services.AddSingleton<ITasksRepository, TasksRepository>();
        }
    }

Option 3 : Environment Based + Configuration Based

    public IConfigurationRoot Configuration { get; set; }
    public IHostingEnvironment env{ get; set; }
    public Startup(IHostingEnvironment env)
    {
        this.env = env;
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();
        Configuration = builder.Build();
    } 
    public void ConfigureServices(IServiceCollection services)
    {
        var isFakeMode = Configuration["ServiceRegistrationMode"] == "Fake";
        if (env.IsDevelopment() && isFakeMode)
        {
            // register other fake dependencies
            services.AddSingleton<ITasksRepository, TasksRepositoryFake>();
        }
        else
        {
            // register other real dependencies
            services.AddSingleton<ITasksRepository, TasksRepository>();
        }
    }

3 Comments

it doesn't allow ConfigureServices to have more than one params, and throws exception: System.InvalidOperationException, with message, The ConfigureServices method must either be parameterless or take only one parameter of type IServiceCollection.
Thanks Adem. But there still is problem in Option 1 and Option 3. Where do you get the variable env from? It is not defined in scope, hence gives exception
better way to do that is by setting different env: Testing, Development, Staging. applying startup conventions and you can reduce the if clause, i.e. ConfigureTestingServices & ConfigureDevelopmentServices

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.