Dependency Injection in ASP.NET Core: Great Best Practices

Dependency injection is an essential aspect of building robust and maintainable applications in ASP.NET Core. It helps to separate concerns, decouple components, and make code more testable, which in turn results in better overall application quality. In this discussion, we are going to look at how to use dependency injection in ASP.NET Core to resolve connection strings and avoid common warnings.

Question: In dotnet core; what is the difference between these 2 code –

builder.Services.AddDbContext(options =>
{
options.UseSqlServer(opt.ConnectionStrings.FISDbConnection, sqlServerOptionsAction: sqlOptions =>
{
});
});
builder.Services.AddDbContext();

this is the full code –

builder.Services.Configure(builder.Configuration);
var serviceProvider = builder.Services.BuildServiceProvider();
var opt = serviceProvider.GetRequiredService>().Value;
builder.Services.AddDbContext(options =>
{
options.UseSqlServer(opt.ConnectionStrings.FISDbConnection, sqlServerOptionsAction: sqlOptions =>
{
});
});

Ans: The code is related to setting up a database context in a .NET Core application.

Here is a brief explanation of the code:

  1. builder.Services.AddDbContext<FISContext>(options => { ... });
  • builder is an instance of HostBuilder class which is used to set up a host for the application.
  • Services property of the builder object is used to add services to the dependency injection container.
  • AddDbContext is an extension method that adds a database context (FISContext in this case) to the dependency injection container.
  • The options parameter is a delegate that provides options for configuring the database context.
  • options.UseSqlServer sets the database provider to be used with the context as SQL Server. The connection string for the database is passed to UseSqlServer as opt.ConnectionStrings.FISDbConnection.
  • The sqlServerOptionsAction parameter allows configuring additional options for the SQL Server database provider. In this case, it is an empty delegate, but you could specify additional options if needed.
  1. builder.Services.AddDbContext<FISContext>();
  • This is a simpler version of the first code snippet. It adds the database context (FISContext) to the dependency injection container without any additional configuration options.

The first code snippet sets up the database context with specific configuration options, such as specifying the database provider and the connection string. The second code snippet sets up the database context with default options, which may not be suitable for all use cases.

The full code sets up a host for the application and configures the database context as part of it. It first sets the FISSettings configuration options using Configure<FISSettings> method and then builds a service provider to get the value of these options. The database context is then added to the dependency injection container with the specified options using AddDbContext.

Question: “which is used to set up a host for the application” – explain this line.

Ans: The HostBuilder class in .NET Core provides a convenient way to set up a host for your application. A host is a container that provides the services needed for the application to run, such as the application’s configuration, logging, and dependency injection container. The host is responsible for initializing the application and running it.

By using the HostBuilder class, you can create a customized host that meets the specific needs of your application. The HostBuilder class provides methods for setting up the configuration, logging, and dependency injection container, and for starting and running the application.

In the code provided, an instance of the HostBuilder class is created and used to set up the host for the application. The host is then built using the Build method, which creates the host and starts it running.

Question:

public class QueryRepository : IQueryRepository where TEntity : BaseDocument
{
protected readonly string _connectionString;
public QueryRepository(string connectionString)
{
_connectionString = connectionString;
}
}

how to dependency inject this class so that it gets connectionstring?

Ans: To dependency inject the QueryRepository class, you can register it with a dependency injection container in your .NET Core application.

Here’s an example of how you could do this using the built-in dependency injection container in .NET Core:

public void ConfigureServices(IServiceCollection services)
{
    // ... other service registrations ...

    services.AddTransient<IQueryRepository<TEntity>, QueryRepository<TEntity>>(x => new QueryRepository<TEntity>(Configuration.GetConnectionString("DefaultConnection")));
}

In this example, the AddTransient method is used to register the QueryRepository class as an implementation of the IQueryRepository interface, with a Transient lifetime. When the dependency injection container creates an instance of IQueryRepository, it will use the QueryRepository class as the implementation.

The QueryRepository constructor takes a connectionString parameter, which is passed to the AddTransient method using a lambda expression. The Configuration.GetConnectionString method is used to retrieve the connection string from the application’s configuration.

With this registration in place, you can now use the IQueryRepository interface in other parts of your application, and the dependency injection container will provide the implementation automatically.

Question: ‘TEntity could not be found’ error is thrown.

ans: The TEntity type parameter is declared in the IQueryRepository interface, but is not available in the ConfigureServices method where you are trying to register the QueryRepository class with the dependency injection container.

You can resolve this issue by either:

  1. Creating a separate registration for each entity type that you want to use with the QueryRepository. For example:
public void ConfigureServices(IServiceCollection services)
{
    // ... other service registrations ...

    services.AddTransient<IQueryRepository<MyEntity>, QueryRepository<MyEntity>>(x => new QueryRepository<MyEntity>(Configuration.GetConnectionString("DefaultConnection")));
    services.AddTransient<IQueryRepository<AnotherEntity>, QueryRepository<AnotherEntity>>(x => new QueryRepository<AnotherEntity>(Configuration.GetConnectionString("DefaultConnection")));
}
  1. Creating a generic factory that can create instances of QueryRepository for any entity type:
public void ConfigureServices(IServiceCollection services)
{
    // ... other service registrations ...

    services.AddTransient(typeof(IQueryRepository<>), typeof(QueryRepository<>));
    services.AddTransient<Func<Type, IQueryRepository>>(x => type =>
    {
        var connectionString = Configuration.GetConnectionString("DefaultConnection");
        var repositoryType = typeof(QueryRepository<>).MakeGenericType(type);
        return (IQueryRepository)Activator.CreateInstance(repositoryType, connectionString);
    });
}

In the second option, the AddTransient method is used to register a factory delegate that takes a Type parameter and returns an instance of IQueryRepository. The factory uses the Activator.CreateInstance method to create instances of QueryRepository for any entity type that is passed to it. The connection string is retrieved from the application’s configuration and passed to the QueryRepository constructor.

Question: What if neither solution works for me?

public interface IQueryRepository where TEntity : BaseDocument
{
Task> GetAsync(string query, bool isProcedure = false);
Task<(long, IEnumerable)> GetMultipleResultAsync(string sql, DynamicParameters parameters, bool isProcedure = false);
}

this is the interface.

public class QueryRepository : IQueryRepository where TEntity : BaseDocument
{
protected readonly string _connectionString;
public QueryRepository(string connectionString)
{
_connectionString = connectionString;
}
}

this is the implementation.

when i try to dependency injection this class like this –
builder.Services.AddScoped, QueryRepository>();
connectionstring is null.

TEntity is not present in program.cs. How can I do the dependency inject this class in program.cs?

ans: In order to use the TEntity type parameter in the ConfigureServices method of your Program.cs file, you need to pass it as a parameter to a factory delegate. Here’s one way to do that:

public void ConfigureServices(IServiceCollection services)
{
    // ... other service registrations ...

    services.AddScoped(typeof(IQueryRepository<>), typeof(QueryRepository<>));
    services.AddScoped<Func<Type, IQueryRepository<BaseDocument>>>(x => type =>
    {
        var connectionString = Configuration.GetConnectionString("DefaultConnection");
        var repositoryType = typeof(QueryRepository<>).MakeGenericType(type);
        return (IQueryRepository<BaseDocument>)Activator.CreateInstance(repositoryType, connectionString);
    });
}

In this example, we’re registering a factory delegate that takes a Type parameter and returns an instance of IQueryRepository<BaseDocument>. The factory uses the Activator.CreateInstance method to create instances of QueryRepository<TEntity> for any entity type that is passed to it. The connection string is retrieved from the application’s configuration and passed to the QueryRepository constructor.

You can then use this factory delegate to obtain instances of IQueryRepository<TEntity> for specific entity types in your application. For example:

var func = serviceProvider.GetRequiredService<Func<Type, IQueryRepository<BaseDocument>>>();
var repository = func(typeof(MyEntity));

In this example, func is a delegate that takes a Type parameter and returns an instance of IQueryRepository<BaseDocument>, and repository is an instance of IQueryRepository<MyEntity>.

Question: What could be the code sample in C# aspnet core?

ans: Here’s an example of how you can implement the solution in an ASP.NET Core application:

  1. Create an interface for the repository:
public interface IQueryRepository<TEntity> where TEntity : BaseDocument
{
    Task<IEnumerable<TEntity>> GetAsync(string query, bool isProcedure = false);
    Task<(long, IEnumerable<TEntity>)> GetMultipleResultAsync(string sql, DynamicParameters parameters, bool isProcedure = false);
}
  1. Create a concrete implementation of the repository:
public class QueryRepository<TEntity> : IQueryRepository<TEntity> where TEntity : BaseDocument
{
    protected readonly string _connectionString;
    public QueryRepository(string connectionString)
    {
        _connectionString = connectionString;
    }

    // Implementation of the IQueryRepository<TEntity> interface goes here
}
  1. In the Startup class, register the repository as a scoped service:
public void ConfigureServices(IServiceCollection services)
{
    // ... other service registrations ...

    services.AddScoped(typeof(IQueryRepository<>), typeof(QueryRepository<>));
    services.AddScoped<Func<Type, IQueryRepository<BaseDocument>>>(x => type =>
    {
        var connectionString = Configuration.GetConnectionString("DefaultConnection");
        var repositoryType = typeof(QueryRepository<>).MakeGenericType(type);
        return (IQueryRepository<BaseDocument>)Activator.CreateInstance(repositoryType, connectionString);
    });
}
  1. In application, use the repository by resolving it from the dependency injection container:
public class MyController : Controller
{
    private readonly Func<Type, IQueryRepository<BaseDocument>> _queryRepositoryFactory;

    public MyController(Func<Type, IQueryRepository<BaseDocument>> queryRepositoryFactory)
    {
        _queryRepositoryFactory = queryRepositoryFactory;
    }

    public async Task<IActionResult> MyAction()
    {
        var repository = _queryRepositoryFactory(typeof(MyEntity));
        var entities = await repository.GetAsync("SELECT * FROM MyTable");
        // Use the entities here...

        return View(entities);
    }
}

In this example, the MyController class has a constructor that takes a Func<Type, IQueryRepository<BaseDocument>> delegate, which is used to obtain instances of IQueryRepository<TEntity> for specific entity types. In this case, the delegate is used to obtain an instance of IQueryRepository<MyEntity> by passing the typeof(MyEntity) to the delegate. The delegate then uses the Activator.CreateInstance method to create an instance of the QueryRepository<MyEntity> class and returns it as an IQueryRepository<BaseDocument>.

Question: still connection string is giving me null.

Can i only modify this line –
builder.Services.AddScoped(typeof(IQueryRepository<>), typeof(QueryRepository<>));
to include a connection string to pass?

Ans:

No, you cannot pass the connection string directly to the AddScoped method. You need to either inject an instance of the connection string into the constructor of the QueryRepository class or use the options pattern as described in my previous answer.

You can modify the AddScoped line to the following:

scssCopy codebuilder.Services.AddScoped(typeof(IQueryRepository<>), typeof(QueryRepository<>));

Then, in the Startup.cs class, you can use the Configure method to set up the connection string:

swiftCopy codebuilder.Services.Configure<AppSettings>(builder.Configuration.GetSection("ConnectionStrings"));
var serviceProvider = builder.Services.BuildServiceProvider();
var opt = serviceProvider.GetRequiredService<IOptions<AppSettings>>().Value;

And in the constructor of the QueryRepository class, you can use the IOptions instance to get the connection string:

javaCopy codepublic QueryRepository(IOptions<AppSettings> options)
{
    _connectionString = options.Value.ConnectionString;
}

Question: Now it’s showing – System.MissingMethodException: 'Cannot dynamically create an instance of type "AppSettings"

ans: The System.MissingMethodException error is thrown because the AppSettings class does not have a default constructor. To resolve this, you can either add a default constructor to the AppSettings class or pass in the AppSettings instance to the constructor of the QueryRepository class.

To add a default constructor to the AppSettings class, add the following code:

public class AppSettings
{
    public string ConnectionString { get; set; }

    public AppSettings()
    {
    }
}

If you don’t want to add a default constructor, you can pass in the AppSettings instance to the constructor of the QueryRepository class. In the Startup.cs file, modify the ConfigureServices method as follows:

builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("ConnectionStrings"));
var serviceProvider = builder.Services.BuildServiceProvider();
var appSettings = serviceProvider.GetRequiredService<IOptions<AppSettings>>().Value;

builder.Services.AddScoped<IQueryRepository<TEntity>, QueryRepository<TEntity>>(serviceProvider => 
    new QueryRepository<TEntity>(appSettings));

And in the constructor of the QueryRepository class, change the constructor signature to the following:

public QueryRepository(AppSettings appSettings)
{
    _connectionString = appSettings.ConnectionString;
}

Question: Why can’t we dependency inject class without default constructor?

ans: In .NET Core, when you use dependency injection, the framework creates an instance of the class for you and injects its dependencies automatically. The framework does this by using reflection to find a constructor that takes the required dependencies as parameters. If the class doesn’t have a constructor that takes the required dependencies, the framework cannot create an instance of the class and will throw an exception.

The default constructor is a constructor that takes no parameters. If a class doesn’t have a default constructor, the framework cannot create an instance of the class using the default constructor, and you need to provide an instance of the class when you register it for dependency injection.

So, when you try to use dependency injection with a class that doesn’t have a default constructor, the framework will throw a System.MissingMethodException with a message indicating that it cannot dynamically create an instance of the class. To resolve this issue, you can either add a default constructor to the class, or you can provide an instance of the class when you register it for dependency injection, for example by using a factory method.

Question: Calling ‘BuildServiceProvider’ from application code results in copy of Singleton warning. How do I avoid this?

ans: We can avoid the warning by not calling BuildServiceProvider from application code. Instead, you can use the IServiceProvider that is provided by the hosting environment, typically through the Host object in ASP.NET Core or through the Startup class in a console application.

For example, in an ASP.NET Core application, you can configure your services in the ConfigureServices method of the Startup class and then use the IServiceProvider that is available through the HttpContext in your controllers:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDatabaseSettings>(_ => new DatabaseSettings(configuration.GetConnectionString("FISDbConnection")));
}
public class MyController : Controller
{
    private readonly IDatabaseSettings _databaseSettings;

    public MyController(IDatabaseSettings databaseSettings)
    {
        _databaseSettings = databaseSettings;
    }

    // ...
}

In a console application, you can use the IServiceProvider that is available through the Startup class:

public class Startup
{
    private readonly IConfiguration _configuration;

    public Startup(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IDatabaseSettings>(_ => new DatabaseSettings(_configuration.GetConnectionString("FISDbConnection")));
    }
}
class Program
{
    static void Main(string[] args)
    {
        var host = Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                var startup = new Startup(hostContext.Configuration);
                startup.ConfigureServices(services);
            })
            .Build();

        using (host)
        {
            // ...
        }
    }
}

Conclusion:

In conclusion, dependency injection is an integral part of building maintainable and scalable applications in ASP.NET Core. It enables the separation of concerns, improves testability, and makes code more modular and easy to maintain. By following the best practices and avoiding common pitfalls, such as resolving connection strings and avoiding warnings, developers can build robust and maintainable applications with ASP.NET Core.

You can find my details here – https://asifulhaque.com/about-me/
You can also check out my repository: https://github.com/asifbuetcse?tab=repositories

Leave a Comment