Skip to content

Providers - Best Coding Practices

The Domain Services is extensible through a number of extensibility points. These extensibility points come in the form of interfaces such as ITimeSeriesRepository, IMapSource and IWorker. Concrete implementations of these interfaces are called providers and act as "plugins" to Domain Services. This document describes some best practices for coding providers.

Using the base classes

If a base class exists, you should always code your provider implementation as an extension of this base class.

The extensibilty points are defined by interfaces, so in principle you could start from scratch implementing that interface. However, most of the provider interfaces are backed up by base classes. If a base class exists, you should always code your provider implementation as an extension of this base class. The base class will provide default implementations of some of the interface members. This has the advantage, that you can establish a functional implementation very quickly by implementing only the mandatory abstract members. The default implementations in the base classes will typically be marked virtual so you can override them in your own implementation, if you can come up with a better implementation yourself.

Using the base classes also help you avoid violating the Liskov's Substitution Principle.

Lean implementations

Provider implementations should preferably be kept as lean as possible and merely play the role as adapters between Domain Services and functionality kept in other (native) libraries.

The fundamental principle of Domain Service - and the MIKE Foundation Libraries in general - is to avoid confining common functionality within the boundaries of a particular framework, such as the MIKE CUSTOMISED Platform or MIKEZero. Rather, the goal is to unleash this functionality for use in any possible context. This is important to keep in mind when implementing providers.

Therefore, provider implementations should preferably be kept as lean as possible and merely play the role as adapters between Domain Services and functionality kept in other (native) libraries.

For example, a time series repository implementation using dfs0-files should be based on functionality within the native MIKEZero library Generic.MIKEZero.Dfs.dll:

public override bool Contains(string id)
{
    var dfs0File = DfsFileFactory.DfsGenericOpen(_filePath);
    var idObjet = Dfs0TimeSeriesRepositoryId.Parse(id);
    var exists = dfs0File.ItemInfo.Any(idObjet.EqualTo);
    dfs0File.Close();
    return exists;
}

Command-Query separation

Queries must be implemented so that they never introduce any side effects.

Domain Services adheres to the Command-Query Separation principle. Every method in the provider interfaces can be easily identified by the method signature as either a query or a command. The return type is the give-away for the difference: queries have a return value - commands don't.

A query:

Maybe<TEntity> Get(TEntityId id);

A command:

void Add(TEntity entity);

The implementations of these methods must adhere to the characteristics of queries and commands respectively. Queries must be implemented so that they never introduce any side effects, which means that they cannot change any observable state of the system. Commands are "modifiers" that change some state of the system.

This is especially important because the providers are plugins to services that might be exposed as RESTful HTTP services through the Web API. In HTTP, safety and idempotency are important terms. For example a GET method must always be safe - in the sense that it does not have any side effects - as well as idempotent, while a POST method guarantees neither.

Liskov's substitution principle

The presence of a NotImplementedException in a provider is a code smell indicating an LSP violation.

Providers are plugins to Domain Services. Thus, it is very important that an implementation of a particular provider interface can be replaced by any other implementation of that interface. in other words, the different provider implementations have to be semantically interoperable. This is known as the Liskov's Substitution Principle (LSP).

A typical LSP violation is to ignore implementation of certain provider interface members - for example by throwing a NotImplementedException. Technically, the member is implemented, but it will potentially cause a runtime error:

public override void Remove(string id)
{
    throw new NotImplementedException();
}

The presence of a NotImplementedException in a provider is a code smell indicating an LSP violation.

LSP is strongly related to the Interface Segregation Principle (ISP) that encourages the use of small so-called role-interfaces instead of large so-called header-interfaces. As Domain Services define the provider interfaces by combinations of relatively small and specific role-interfaces, no provider should be forced to implement irrelevant members. For example the IUpdatableRepository interface is an extension of the IDiscreteRepository interface:

public interface IUpdatableRepository<TEntity, TEntityId> 
    : IDiscreteRepository<TEntity, TEntityId> where TEntity : IEntity<TEntityId>
{
    void Add(TEntity entity);
    void Remove(TEntityId id);
    void Update(TEntity entity);
}

Therefore, if you want to implement a repository that does not support add, remove and updates, you are not forced to do so. You can stick to an IDiscreteRepository implementation instead. This should help you avoid LSP violations.

Robustness and trust

Be conservative in what you return, be liberal in what you accept.

A provider is a plugin and as such an extension of existing functionality. Therefore it is important to establish trust that the provider implementation is robust and reliable.

The Robustness Principle – also known as Postel's Law - states something like: "Be conservative in what you return, be liberal in what you accept". This sets the scene regarding how to handle input and output in a provider implementation.

Input

Use fail-fast techniques such as validating constructor arguments in guard clauses and throwing exceptions if failure is detected:

public TableRepository(string connectionString)
{
    if (string.IsNullOrEmpty(connectionString))
    {
        throw new ArgumentException("Cannot be null or empty.", "connectionString");
    }

    ...
}

Write detailed and descriptive exception messages targeted towards other programmers. It is important to realize that exception messages are for programmers - not for end users:

throw new ArgumentException("Could not parse time series id string '" + s +
                            "'. A times series id has the following format:\n\n" +
                            "<path>;<item>\n\n" +
                            "<path> is the relative file path - e.g. data/test.csv\n" +
                            "<item> is the item name - e.g. WaterLevel\n\n" +
                            "Time series id example: data/test.csv;WaterLevel", "s");

Output

The stronger a guarantee you can give about the output of your provider implementation, the better. Generally, you should consider null an invalid return value for any method. You cannot necessarily expect the consumers of the providers (the services) to handle null references. For some interface methods, this is very explicit as they have the return type of a so-called Maybe:

Maybe<TEntity> Get(TEntityId id);

This method signature clearly signals that the Get-method should be implemented in such a way that it might – or might not – return an actual entity, but it should never return a null reference. The Maybe-concept is described in greater details in this blog post.

Assembly strategy

The providers are separated in assemblies (dlls) according to the dependencies that they introduce.

All providers must be defined in the DHI.Services.Provider namespace (or sub namespaces). The providers are separated in assemblies (dlls) according to the dependencies that they introduce. For example, the DHI.Services.Provider.DIMS is an assembly containing all the extensibility point implementations that depend on the DIMS.CORE API (DHI.DIMS.CORE.API.dll). Likewise, the DHI.Services.Provider.MIKE assembly contains implementations that depend on the MIKE Core libraries - such as DHI.Generic.MikeZero.DFS.dll, DHI.Chart.Map.dll etc.

Extensibility point implementations that do not introduce any dependencies should reside in the DHI.Services.Provider.dll assembly.

Unit testing

Every provider assembly should be accompanied by an equivalent test assembly. For example the DHI.Services.Provider.DIMS assembly is accompanied by a DHI.Services.Provider.DIMS.Test assembly containing unit tests for all of the types in the DHI.Services.Provider.DIMS namespace.

The unit testing shall be done using the xUnit.net testing framework.

Remember to treat your test code with the same respect as your production code. Remember to test for both positive and negative cases - e.g. that exceptions are correctly thrown under given circumstances.

The unit tests are obviously important to ensure the quality and robustness of the providers, but they also provide enhanced security when refactoring code.

Code Style

You should adhere to the currently used code style. This code style is essentially equivalent to the default rules of ReSharper. If you do not have a license to ReSharper, alternatively you can use free tools such as StyleCop and CodeMaid.

Notable code style paradigms are:

  • All using statements must be placed in the deepest scope (within the namespace)
  • Preferably, use implicitely typed variables (the var keyword)

XML documentation

Consider using XML documentation of types - for example to communicate the format of a repository connection string or the format of an entity ID.

The below XML documentation is describing the format or the time series identifier for a time series repository comprising dfs0-files in a hierarchical folder structure.

/// <summary>
///     Parses the specified time series ID for 
///     a Dfs0GroupedTimeSeriesRepository.
/// </summary>
/// <param name="s">
///     The time series ID.
///     A times series ID has the following format:<para/>
///     &lt;path&gt;;&lt;item&gt;<para/>
///     where &lt;path&gt; is the relative file path - e.g. "data/test.dfs0"
///     and &lt;item&gt; is the item name - e.g. "LevelTS"<para/>
///     Example: "data/test.dfs0;LevelTS";
/// </param>
/// <returns>An instance of a Dfs0GroupedTimeSeriesId.</returns>
public static Dfs0GroupedTimeSeriesId Parse(string s)
{
   ...
}

This XML documentation results in documentation like below: