Building a typed DbConnection factory
Let say we have 2 databases, one for the business entities and one for the audit log, and let say we have 2 services, the business entity store is using SqlServer specific features while the audit log writer only insert records in the database in plain SQL.
Now, when implementing a service using a database, the simplest way to get the DbConnection
we require is to get the connection string from IConfiguration
and instanciate the DbConnection of the required type.
These information can only be found in the code or in the documentation, but not in the signature of the constructor of the service.
As the implementer of a service using a database, we would like to express the type of DbConnection
we require and be given the connection string we will be using.
ILogger<TCategoryName>
and ILoggerFactory
show a way to explicit in the type the name of the logger.
We can first define a IDbConnectionFactory
that will return a new a DbConnection
when given a specified name.
// <summary>
/// Represents a type used to create instances of <see cref="DbConnection"/> based on the given name.
/// </summary>
public interface IDbConnectionFactory
{
/// <summary>
/// Creates a new <typeparamref name="TConnection"/> instance using the given <paramref name="name"/>.
/// </summary>
/// <param name="name">The name of the connection.</param>
/// <returns>The <typeparamref name="TConnection"/> that was created.</returns>
DbConnection Create(string name);
}
Then we can define the generic IDbConnectionFactory<out TCategoryName>
and an implementation that will call the previously defined factory.
/// <summary>
/// A generic interface for creating <see cref="DbConnection"/> where the category name is derived
/// from the specified <typeparamref name="TCategoryName"/> type name.
/// </summary>
/// <typeparam name="TCategoryName">The type who's name is used for the factory category name.</typeparam>
public interface IDbConnectionFactory<out TCategoryName>
{
/// <summary>
/// Creates a <see cref="DbConnection"/>.
/// </summary>
/// <returns>The newly created <see cref="DbConnection"/>.</returns>
DbConnection Create();
}
/// <summary>
/// Delegates the create of the <see cref="DbConnection"/> to the provided <see cref="IDbConnectionFactory"/>.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
public class DbConnectionFactory<T> : IDbConnectionFactory<T>
{
private readonly IDbConnectionFactory _factory;
/// <summary>
/// Creates a new instance.
/// </summary>
/// <param name="factory">The factory.</param>
public DbConnectionFactory(IDbConnectionFactory factory)
{
_factory = factory;
}
/// <inheritdoc/>
public DbConnection Create()
{
return _factory.Create(typeof(T).Name);
}
}
This is fine but it solves only half of the problem. How can we explicit the type of DbConnection we require?
The idea came from the extension Open
when wanted to write to directly create and open the connection:
public static DbConnection Open<T>(this IDbConnectionFactory<T> factory)
{
var connection = factory.Create();
connection.Open();
return connection;
}
What if, instead of T, we used an generic interface constrained on a DbConnection
, something like IDbRequire<TConnection>
?
The extension Open
could then be implemented as
/// <summary>
/// Opens a database connection for the given factory.
/// </summary>
/// <typeparam name="TConnection">The type of connection to open.</typeparam>
/// <param name="factory">The connection factory.</param>
/// <returns>The connection of the given type, opened.</returns>
public static TConnection Open<TConnection>(this IDbConnectionFactory<IDbRequire<TConnection>> factory)
where TConnection : DbConnection
{
var connection = (TConnection)factory.Create();
connection.Open();
return connection;
}
The definition of IDbRequire<TConnection>
is trivial.
/// <summary>
/// Marker interface to type the required <see cref="DbConnection"/>.
/// </summary>
/// <typeparam name="TConnection">The required type of connection.</typeparam>
public interface IDbRequire<TConnection>
where TConnection : DbConnection
{
}
Putting it all together yield the following test.
public class DbConnectionFactoryTests
{
[Fact]
public void Can_resolve_a_factory_of_DbConnection_of_the_required_type()
{
var services = new ServiceCollection();
services.TryAddSingleton(typeof(DbConnectionFactory<>));
services.AddSingleton<IDbConnectionFactory, StubConnectionFactory>();
var provider = services.BuildServiceProvider();
var factory = provider.GetService<DbConnectionFactory<Trait>>();
Assert.NotNull(factory);
using (var connection = factory.Create())
{
Assert.IsType<SQLiteConnection>(connection);
}
}
#region Test suite helpers
private class Trait : IDbRequire<SQLiteConnection> { }
private class StubConnectionFactory : IDbConnectionFactory
{
public DbConnection Create(string name)
{
return name switch
{
nameof(Trait) => new SQLiteConnection("Filename=:memory:"),
_ => throw new ArgumentOutOfRangeException(nameof(name)),
};
}
}
#endregion
}