How can you minimize the impact of the persistence layer on your domain models?

This is a blog response to the question ‘How can you minimize the impact of the persistence layer on your domain models?’ posted on reddit /r/dotnet, showing how I would have implemented the solution in the purest form of OOP that is possible with C#.

(Note: The implementation of the factory and persistence classes is simplified for demonstration purposes.)

So here is the code. It is tested with .NET 5 on Windows 10.

using System;
using System.Data;
using System.Diagnostics;

namespace Example.Domain
{
    // Let's pretend this namespace is in a separate assembly so that
    // the 'internal' scope modifier applies

    public interface IAccount
    {
	string Username { get; }

	bool IsEnabled { get; }

	void Disable();
    }

    public interface ISavedAccount : IAccount
    {
	int Id { get; }
	
	void Enable();
    }

    // Not accessible from outside assembly
    internal abstract class AccountBase : IAccount
    {
	public string Username { get; protected set; }

	public bool IsEnabled { get; protected set; }

	internal AccountBase(string username)
	{
	    Username = username;
	}

	public void Disable()
	{
	    IsEnabled = false;
	}
    }

    // Not accessible from outside assembly
    internal class MyAccount : AccountBase
    {
	internal MyAccount(string username)
	    : base(username)
	{
	}
    }

    // Not accessible from outside assembly
    internal class MySavedAccount : AccountBase, ISavedAccount
    {
	public int Id { get; private set; }

	internal MySavedAccount(int id, string username, bool isEnabled)
	    : base(username)
	{
	    Id = id;
	    if (isEnabled) 
	    {
		Enable();
	    }
	}

	public void Enable()
	{
	    IsEnabled = true;
	}
    }

    public static class AccountFactory
    {
	public static IAccount Create(string username)
	{
	    return new MyAccount(username);
	}

	public static ISavedAccount Create(int id, string username, bool isEnabled)
	{
	    return new MySavedAccount(id, username, isEnabled);
	}
    }
}

namespace Example.Persistence
{
    using Example.Domain;

    public static class AccountStorage
    {
	static readonly DataTable dt;

	static AccountStorage()
	{
	    dt = new DataTable();
	    dt.Columns.Add("id", typeof(int));
	    dt.Columns.Add("username", typeof(string));
	    dt.Columns.Add("enabled", typeof(bool));
	}

	public static DataRow AddAccountRow(ref IAccount account)
	{
	    var dr = dt.NewRow();
	    dr["id"] = dt.Rows.Count+1;
	    dr["username"] = account.Username;
	    dr["enabled"] = account.IsEnabled;
	    dt.Rows.Add(dr);

	    // This variable does not point to a valid account after it has been saved
	    account = null;

	    return dr;
	}

	public static DataRow UpdateAccountRow(ISavedAccount account)
	{
	    var dr = FindAccountRow(account.Id);
	    dr["username"] = account.Username;
	    dr["enabled"] = account.IsEnabled;
	    return dr;
	}

	public static DataRow FindAccountRow(int id)
	{
	    for (var i=0; i<dt.Rows.Count; ++i)
	    {
		DataRow dr = dt.Rows[i];
		if (((int)dr["id"]) == id)
		{
		    return dr;
		}
	    }
	    return null;
	}
    }
}

namespace Example.Client
{
    // Let's pretend this namespace is in a separate assembly so that it
    // doesn't see the 'internal' definitions from the Example.Domain
    // namespace

    using Example.Domain;
    using Example.Persistence;

    class Program
    {
        static void Main(string[] args)
        {
	    var account = AccountFactory.Create("joebloggs");

	    // FAIL! A new account does not have an id
	    // Debug.Assert(account.Id > 0);

	    // FAIL! A new account cannot be enabled
	    // account.Enable();

	    var newrow = AccountStorage.AddAccountRow(ref account);

            Debug.Assert(account == null, "The account object is no longer valid");

	    Debug.Assert(((int)newrow["id"]) > 0, "The 'id' value is set");
	    Debug.Assert("joebloggs".Equals(newrow["username"]), "The 'username' value is set");
	    Debug.Assert(! (bool)newrow["enabled"], "The 'enabled' value is not set");

	    var savedaccount = AccountFactory.Create((int)newrow["id"], (string)newrow["username"], (bool)newrow["enabled"]);
	    savedaccount.Enable();
	    Debug.Assert(savedaccount.IsEnabled, "The account is enabled");

	    var unused = AccountStorage.UpdateAccountRow(savedaccount);

	    var loadedrow = AccountStorage.FindAccountRow(savedaccount.Id);
	    var loaded = AccountFactory.Create((int)loadedrow["id"], (string)loadedrow["username"], (bool)loadedrow["enabled"]);

	    Debug.Assert(loaded.Id > 0, "An id is assigned");
	    Debug.Assert("joebloggs".Equals(loaded.Username), "The username is set correctly");
	    Debug.Assert(loaded.IsEnabled, "The account is enabled");
        }
    }
}

The line below might be questionable because we are effectively passing ownership of the account object to the persistence layer, but we could assume that there is a contract that formalises this.

	    // This variable does not point to a valid account after it has been saved
	    account = null;

Leave a comment

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.