This post answers 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 implement a solution in the purest OOP [sic] possible with C#.
(Note: The factory and persistence classes are 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 could be questionable because we pass ownership of object account
to the persistence layer, but we could also 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;