The Kladde’s API tests have run against an in-memory store since lesson 2.7.3. That makes them fast, and they don’t need a SQL Server. In production, though, the Kladde runs with SQL Server. So a green test says: “The API works with the in-memory store.” It says nothing about whether it works with SQL Server, unless both stores behave the same. That’s exactly what the L in SOLID is about, the Liskov substitution principle: every implementation of an interface must be usable wherever the interface is required, without changing anything a caller may rely on.

In this lesson you write tests that check exactly that, and you find two places where the Kladde didn’t hold to it.

An interface’s contract

An interface in C# fixes names, parameters and return types. That’s the smaller part of the contract. The larger part isn’t in the type system:

  • Preconditions: what the caller has to supply. “The name isn’t empty.”
  • Postconditions: what the implementation guarantees afterward. “A contact that was added is found by the search.” “The results are sorted by name.”
  • Exceptions and errors: how it fails. “An address that’s already taken returns a Result with contact.email.taken, not an exception.”

In this language, the principle reads: an implementation must not strengthen the preconditions and must not weaken the postconditions. It mustn’t demand more and mustn’t guarantee less than the contract. And it mustn’t fail in any way other than the contract says.

For the Kladde’s contacts, the contract looks like this, src/Kladde.Core/IContactStore.cs:

C#
namespace Kladde.Core;

/// <summary>Where the office keeps its contacts. What every store must do stands here and, better, in ContactStoreContract.</summary>
public interface IContactStore
{
    /// <summary>Adds a contact. An address that another contact has, ignoring case, is refused with contact.email.taken.</summary>
    Task<Result<Contact>> AddAsync(string name, string email, CancellationToken cancellationToken);

    /// <summary>The contacts whose name contains the term, ignoring case, sorted by name.</summary>
    Task<IReadOnlyList<Contact>> SearchAsync(string term, CancellationToken cancellationToken);
}

If this port has a different name in your project, or your endpoints still talk directly to KladdeBook and ContactDirectory, that’s the first small refactoring of this lesson: a port with the methods the contacts endpoints need, and the two stores behind it. The comments on the methods are the contract in words. They have one drawback: nobody checks them.