Search code examples
c#.netoopcode-contractsdesign-by-contract

How to ensure that implementations of an interface have a connection string if they are backed by a database using code contracts


Imagine you've an interface like this:

public interface IPersonManager
{
     public void AddPerson(string name);
}

...and an implementation which we'll going to call DefaultPersonManager. Let's say we want to be sure that any implementation of IPersonManager won't be able to give a null or empty string as argument of AddPerson(string name). For that matter, we're going to implement a contract class as follows:

[ContractClassFor(typeof(IPersonManager))]
public abstract class IPersonManagerContract : IPersonManager
{
   public void AddPerson(string name)
   {
       Contract.Requires(!string.IsNullOrEmpty(name), "Person's name cannot be a null or empty string");
   }
}

...and we'll decorate our IPersonManager interface with the ContractClassAttribute attribute:

[ContractClass(typeof(IPersonManagerContractClass))]
public interface IPersonManager
{
     public void AddPerson(string name);
}

We talked about a DefaultPersonManager. It would look like this class:

public class DefaultPersonManager
{
    private readonly List<string> _personNames = new List<string>();

    public void AddPerson(string name)
    {   
        // "name" argument will be verified by contract class!
        _personNames.Add(name);
    }
}

Alright!

Now we need to implement a new IPersonManager implementation which differs from the DefaultPersonManager in that AddPerson should persist person names to a SQL database (i.e. SQL Server, it's just an example...). We'll call this implementation DbBackedPersonManager.

Since DbBackedPersonManager requires a connection string, we could add a pre-condition in the AddPerson method implementation of DbBackedPersonManager:

public void AddPerson(string name)
{
     Contract.Requires(ConfigurationManager.ConnectionStrings["SomeConnectionStringId"] != null, "A connection string is required in your application/web configuration file");
}

Wrong: code contracts compiler will say that AddPerson implements an interface member thus we can't add a Requires (Read this Q&A I found that was answered by Jon Skeet and it's someway related to this topic a long time ago.).

How would be able to ensure that a specific implementation mandatorily requires a connection string to work nicely?


Solution

  • Possibly an approach would be creating an unrelated interface called IWithSqlDbBackend (or any identifier of your preference...) like this:

    public interface IWithSqlDbBackend
    {
         string ConnectionString { get; }
         string ConnectionStringId { get; set; }
    }
    

    Later, we'll need to create a contract class like this:

    [ContractClassFor(typeof(IWithSqlDbBackend))]
    public abstract class IWithSqlDbBackendContract : IWithSqlDbBackend
    {
        public string ConnectionString
        {
            get 
            {  
                Contract.Requires(!string.IsNullOrEmpty(ConnectionStringId), "Connection string id cannot be null or empty");
                Contract.Requires(ConfigurationManager.ConnectionStrings[ConnectionStringId] != null, "Connection string must be configured");
                Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()), "A connection string cannot be null");
    
                return null;
            }
        }
    
        public string ConnectionStringId
        {
            get
            {
                Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()), "A connection string identifier cannot be null or empty");
    
                return null;
            }
        }
    }
    

    ...also we'll need to decorate IWithSqlDbBackend interface with the so-called ContractClassAttribute:

    [ContractClass(typeof(IWithSqlDbBackendContract))]
    public interface IWithSqlDbBackend
    {
        ...
    }
    

    ...and implement the interface in DbBackedPersonManager. I'll add here the implementation signature:

    public class DbBackedPersonManager : IPersonManager, IWithSqlDbBackend
    

    Finally, if we create an instance of DbBackedPersonManagerand we try to call AddPerson method implementation but no connection string was previously configured in the application/web configuration file (i.e. web.config or app.config...), our pre-conditions will ensure that our application, service or library isn't satisfying the contract to work with persons stored in a database backend!

    Side note

    This is just a sample of how a lot of other domains would be able to ensure a bunch of conditions that, due to code contracts contract classes limitations, would be impossible to verify using regular polymorphism and code contracts.