0

Let's assume you have a project (e.g. .NET Core) using dependency injection. A database repository relies on a database connection string.

public abstract class BaseRepository
{
    // use this when accessing the database
    private readonly string connectionString;
    
    protected BaseRepository(string connectionString)
    {
        this.connectionString = connectionString;
    }
}

You don't want to pass this variable on every call so you simply add it during the DI container setup

// connectionString is taken from a config file
services.AddTransient<IUsersRepository>(serviceProvider => new UsersRepository(connectionString));

But let's assume you have a class that could be used for multiple cases e.g. a class that hashes some things. I created a sample class for this question

public class KeyEncryptor
{
    private readonly KeyDerivationPrf keyDerivationPrf;
    private readonly int iterationCount;
    private readonly int desiredKeyLength;
    
    public KeyEncryptor(KeyDerivationPrf keyDerivationPrf, int iterationCount, int desiredKeyLength)
    {
        this.keyDerivationPrf = keyDerivationPrf;
        this.iterationCount = iterationCount;
        this.desiredKeyLength = desiredKeyLength;
    }
    
    public byte[] Encrypt(string content, byte[] saltBytes)
    {
        return KeyDerivation.Pbkdf2(content, saltBytes, keyDerivationPrf, iterationCount, desiredKeyLength);
    }
    
    public bool Compare(string rawContent, byte[] encryptedKey, byte[] saltBytes)
    {
        byte[] encryptedContentKey = Encrypt(rawContent, saltBytes);
        string encryptedContentKeyText = Convert.ToBase64String(encryptedContentKey);
        string encryptedKeyText = Convert.ToBase64String(encryptedKey);

        return encryptedContentKeyText.Equals(encryptedKeyText);
    }
}

As you can see here many parameters are passed from the config file to the constructor. Now one could say

If you would remove those constructor parameters and expect them as method parameters your class would be more flexible

but

  • then other classes would have to pass in the whole configuration values on every call
  • then I would also have to pass in the connection string every time to the repository to make it uniform

You might know that the connection string or hashing algorithm never changes so you set it once during startup. But others might say keep it as flexible as possible, don't bother passing in 10 parameters on every call.

So when should you set constants during DI setup and when expect them on the fly as parameters? Is there a rule to follow?

  • “Flexible” and “easy to use” are opposite design goals. Obviously, in order for something to be more flexible, the caller is going to have to specify more things. – John Wu Aug 8 at 18:57
2

ASP.NET Core has the Options pattern, which lets you inject your configuration using a strongly-typed class wrapped in an IOptions<T>.

In your example, you would change your constructor from

public KeyEncryptor(KeyDerivationPrf keyDerivationPrf, int iterationCount, int desiredKeyLength)

to

public KeyEncryptor(IOptions<KeyEncryptorOptions> options)

where KeyEncryptorOptions is a class that contains the parameters as properties. You can configure the KeyEncryptorOptions class during startup from your configuration and inject as if it were any other dependency.

| improve this answer | |
3

Why choose? Assuming YAGNI doesn't apply here (it probably does, honestly), you can create a factory that supports both. Inject that instead.

interface IEncryptorFactory
{
    //Creates an encryptor initialized with default settings (e.g. from config)
    Encryptor GetEncryptor();

    //Creates an encryptor with the specified settings
    Encryptor GetEncryptor(KeyDerivationPrf keyDerivationPrf, int iterationCount, int desiredKeyLength);
}


class EncryptorFactory : IEncryptorFactory
{
    protected readonly IConfiguration _configuration;  //Injected

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

    public Encryptor GetEncryptor() 
    {
        return new Encryptor
        (
            _configuration.KeyDerivationPrf, 
            _configuration.IterationCount, 
            _configuration.DesiredKeyLength
        );
    }

    public Encryptor GetEncryptor(KeyDerivationPrf keyDerivationPrf, int iterationCount, int desiredKeyLength)
    {
        return new Encryptor
        (
            keyDerivationPrf, 
            iterationCount, 
            desiredKeyLength
        );
    }
}
| improve this answer | |
0

I would not say that there is a rule to follow. The more flexible you create the design the more burden you put on your consumers.

One path could be to add a factory to the DI and have the construction of the objects moved there. That will allow you to inject all the constants to your factory in the DI container. And at the same time allow the consumers the flexibility to create a fully custom object if the factory methods aren't suited.

Something along the lines of:

public class KeyEncryptorFactory {
    public KeyEncryptorFactory(KeyDerivationPrf keyDerivationPrfForTypeXYZ, int iterationCountForTypeXYZ, int desiredKeyLengthForTypeXYX, KeyDerivationPrf keyDerivationPrfForTypeABC, int iterationCountForTypeABC, int desiredKeyLengthForTypeABC){
        // ....
    }
    public KeyEncryptor GetXYZ(){
        // ....
    }
    public KeyEncryptor GetABC(){
        // ....
    }
}
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.