CRUD Operations on Immutable Objects

These scenarios demonstrate how to perform Create, Read, Update, and Delete operations on immutable objects.

Scenario Prototype

public interface IImmutableScenario<TReadOnlyModel>
   where TReadOnlyModel : class, IReadOnlyEmployeeClassification
{
    /// <summary>
    /// Create a new EmployeeClassification row, returning the new primary key.
    /// </summary>
    int Create(TReadOnlyModel classification);

    /// <summary>
    /// Delete a EmployeeClassification row using an object.
    /// </summary>
    /// <remarks>Behavior when row doesn't exist is not defined.</remarks>
    void Delete(TReadOnlyModel classification);

    /// <summary>
    /// Gets an EmployeeClassification row by its name. Assume the name is unique.
    /// </summary>
    /// <remarks>Must return a null if when row doesn't exist.</remarks>
    TReadOnlyModel? FindByName(string employeeClassificationName);

    /// <summary>
    /// Gets all EmployeeClassification rows.
    /// </summary>
    IReadOnlyList<TReadOnlyModel> GetAll();

    /// <summary>
    /// Gets an EmployeeClassification row by its primary key.
    /// </summary>
    /// <remarks>Behavior when row doesn't exist is not defined.</remarks>
    TReadOnlyModel? GetByKey(int employeeClassificationKey);

    /// <summary>
    /// Update a EmployeeClassification row.
    /// </summary>
    /// <remarks>Behavior when row doesn't exist is not defined.</remarks>
    void Update(TReadOnlyModel classification);
}
public interface IReadOnlyEmployeeClassification
{
    int EmployeeClassificationKey { get; }
    string? EmployeeClassificationName { get; }
    bool IsEmployee { get; }
    bool IsExempt { get; }
}

ADO.NET

Since ADO doesn't directly interact with models, no changes are needed for immutable objects other than to call a constructor instead of setting individual properties.

public class ImmutableScenario : SqlServerScenarioBase, IImmutableScenario<ReadOnlyEmployeeClassification>
{
    public ImmutableScenario(string connectionString) : base(connectionString)
    { }

    public int Create(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        const string sql = @"INSERT INTO HR.EmployeeClassification (EmployeeClassificationName, IsExempt, IsEmployee)
                    OUTPUT Inserted.EmployeeClassificationKey
                    VALUES(@EmployeeClassificationName, @IsExempt, @IsEmployee )";

        using (var con = OpenConnection())
        using (var cmd = new SqlCommand(sql, con))
        {
            cmd.Parameters.AddWithValue("@EmployeeClassificationName", classification.EmployeeClassificationName);
            cmd.Parameters.AddWithValue("@IsExempt", classification.IsExempt);
            cmd.Parameters.AddWithValue("@IsEmployee", classification.IsEmployee);
            return (int)cmd.ExecuteScalar();
        }
    }

    public void Delete(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        const string sql = @"DELETE HR.EmployeeClassification WHERE EmployeeClassificationKey = @EmployeeClassificationKey;";

        using (var con = OpenConnection())
        using (var cmd = new SqlCommand(sql, con))
        {
            cmd.Parameters.AddWithValue("@EmployeeClassificationKey", classification.EmployeeClassificationKey);
            cmd.ExecuteNonQuery();
        }
    }

    public void DeleteByKey(int employeeClassificationKey)
    {
        const string sql = @"DELETE HR.EmployeeClassification WHERE EmployeeClassificationKey = @EmployeeClassificationKey;";

        using (var con = OpenConnection())
        using (var cmd = new SqlCommand(sql, con))
        {
            cmd.Parameters.AddWithValue("@EmployeeClassificationKey", employeeClassificationKey);
            cmd.ExecuteNonQuery();
        }
    }

    public ReadOnlyEmployeeClassification? FindByName(string employeeClassificationName)
    {
        const string sql = @"SELECT ec.EmployeeClassificationKey, ec.EmployeeClassificationName, ec.IsExempt, ec.IsEmployee
                    FROM HR.EmployeeClassification ec
                    WHERE ec.EmployeeClassificationName = @EmployeeClassificationName;";

        using (var con = OpenConnection())
        using (var cmd = new SqlCommand(sql, con))
        {
            cmd.Parameters.AddWithValue("@EmployeeClassificationName", employeeClassificationName);
            using (var reader = cmd.ExecuteReader())
            {
                if (!reader.Read())
                    return null;

                return new ReadOnlyEmployeeClassification(
                    reader.GetInt32(reader.GetOrdinal("EmployeeClassificationKey")),
                    reader.GetString(reader.GetOrdinal("EmployeeClassificationName")),
                    reader.GetBoolean(reader.GetOrdinal("IsExempt")),
                    reader.GetBoolean(reader.GetOrdinal("IsEmployee"))
                );
            }
        }
    }

    public IReadOnlyList<ReadOnlyEmployeeClassification> GetAll()
    {
        const string sql = @"SELECT ec.EmployeeClassificationKey, ec.EmployeeClassificationName, ec.IsExempt, ec.IsEmployee FROM HR.EmployeeClassification ec;";

        var result = new List<ReadOnlyEmployeeClassification>();

        using (var con = OpenConnection())
        using (var cmd = new SqlCommand(sql, con))
        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                result.Add(new ReadOnlyEmployeeClassification(
                    reader.GetInt32(reader.GetOrdinal("EmployeeClassificationKey")),
                    reader.GetString(reader.GetOrdinal("EmployeeClassificationName")),
                    reader.GetBoolean(reader.GetOrdinal("IsExempt")),
                    reader.GetBoolean(reader.GetOrdinal("IsEmployee"))
                ));
            }

            return result.ToImmutableArray();
        }
    }

    public ReadOnlyEmployeeClassification? GetByKey(int employeeClassificationKey)
    {
        const string sql = @"SELECT ec.EmployeeClassificationKey, ec.EmployeeClassificationName, ec.IsExempt, ec.IsEmployee
                    FROM HR.EmployeeClassification ec
                    WHERE ec.EmployeeClassificationKey = @EmployeeClassificationKey;";

        using (var con = OpenConnection())
        using (var cmd = new SqlCommand(sql, con))
        {
            cmd.Parameters.AddWithValue("@EmployeeClassificationKey", employeeClassificationKey);
            using (var reader = cmd.ExecuteReader())
            {
                if (!reader.Read())
                    return null;

                return new ReadOnlyEmployeeClassification(
                    reader.GetInt32(reader.GetOrdinal("EmployeeClassificationKey")),
                    reader.GetString(reader.GetOrdinal("EmployeeClassificationName")),
                    reader.GetBoolean(reader.GetOrdinal("IsExempt")),
                    reader.GetBoolean(reader.GetOrdinal("IsEmployee"))
                );
            }
        }
    }

    public void Update(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        const string sql = @"UPDATE HR.EmployeeClassification
                    SET EmployeeClassificationName = @EmployeeClassificationName, IsExempt = @IsExempt, IsEmployee = @IsEmployee
                    WHERE EmployeeClassificationKey = @EmployeeClassificationKey;";

        using (var con = OpenConnection())
        using (var cmd = new SqlCommand(sql, con))
        {
            cmd.Parameters.AddWithValue("@EmployeeClassificationKey", classification.EmployeeClassificationKey);
            cmd.Parameters.AddWithValue("@EmployeeClassificationName", classification.EmployeeClassificationName);
            cmd.Parameters.AddWithValue("@IsExempt", classification.IsExempt);
            cmd.Parameters.AddWithValue("@IsEmployee", classification.IsEmployee);
            cmd.ExecuteNonQuery();
        }
    }
}

Chain

Chain natively supports working with immutable objects, no conversions are needed.

To populate immutable objects, use either the InferConstructor option or a .WithConstructor<...> link to indicate that a non-default constructor should be used.

public class ImmutableScenario : IImmutableScenario<ReadOnlyEmployeeClassification>
{
    readonly SqlServerDataSource m_DataSource;

    public ImmutableScenario(SqlServerDataSource dataSource)
    {
        m_DataSource = dataSource;
    }

    public int Create(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        return m_DataSource.Insert(classification).ToInt32().Execute();
    }

    public void Delete(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        m_DataSource.Delete(classification).Execute();
    }

    public ReadOnlyEmployeeClassification? FindByName(string employeeClassificationName)
    {
        return m_DataSource.From<ReadOnlyEmployeeClassification>(new { employeeClassificationName })
            .ToObjectOrNull(RowOptions.InferConstructor).Execute();
    }

    public IReadOnlyList<ReadOnlyEmployeeClassification> GetAll()
    {
        return m_DataSource.From<ReadOnlyEmployeeClassification>()
            .ToImmutableArray(CollectionOptions.InferConstructor).Execute();
    }

    public ReadOnlyEmployeeClassification? GetByKey(int employeeClassificationKey)
    {
        return m_DataSource.GetByKey<ReadOnlyEmployeeClassification>(employeeClassificationKey)
            .ToObjectOrNull<ReadOnlyEmployeeClassification>(RowOptions.InferConstructor).Execute();
    }

    public void Update(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        m_DataSource.Update(classification).Execute();
    }
}

Dapper

Dapper natively supports working with immutable objects, no conversions are needed.

No special handling is needed to call a non-default constructor.

public class ImmutableScenario : ScenarioBase, IImmutableScenario<ReadOnlyEmployeeClassification>
{
    public ImmutableScenario(string connectionString) : base(connectionString)
    {
    }

    public int Create(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        var sql = @"INSERT INTO HR.EmployeeClassification (EmployeeClassificationName, IsExempt, IsEmployee)
                    OUTPUT Inserted.EmployeeClassificationKey
                    VALUES(@EmployeeClassificationName, @IsExempt, @IsEmployee )";

        using (var con = OpenConnection())
            return con.ExecuteScalar<int>(sql, classification);
    }

    public void Delete(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        var sql = @"DELETE HR.EmployeeClassification WHERE EmployeeClassificationKey = @EmployeeClassificationKey;";

        using (var con = OpenConnection())
            con.Execute(sql, classification);
    }

    public void DeleteByKey(int employeeClassificationKey)
    {
        var sql = @"DELETE HR.EmployeeClassification WHERE EmployeeClassificationKey = @EmployeeClassificationKey;";

        using (var con = OpenConnection())
            con.Execute(sql, new { employeeClassificationKey });
    }

    public ReadOnlyEmployeeClassification? FindByName(string employeeClassificationName)
    {
        var sql = @"SELECT  ec.EmployeeClassificationKey, ec.EmployeeClassificationName, ec.IsExempt, ec.IsEmployee
                    FROM HR.EmployeeClassification ec
                    WHERE ec.EmployeeClassificationName = @EmployeeClassificationName;";

        using (var con = OpenConnection())
            return con.QuerySingleOrDefault<ReadOnlyEmployeeClassification>(sql, new { employeeClassificationName });
    }

    public IReadOnlyList<ReadOnlyEmployeeClassification> GetAll()
    {
        var sql = @"SELECT ec.EmployeeClassificationKey, ec.EmployeeClassificationName, ec.IsExempt, ec.IsEmployee FROM HR.EmployeeClassification ec;";

        using (var con = OpenConnection())
            return con.Query<ReadOnlyEmployeeClassification>(sql).ToImmutableList();
    }

    public ReadOnlyEmployeeClassification? GetByKey(int employeeClassificationKey)
    {
        var sql = @"SELECT ec.EmployeeClassificationKey, ec.EmployeeClassificationName, ec.IsExempt, ec.IsEmployee
                    FROM HR.EmployeeClassification ec
                    WHERE ec.EmployeeClassificationKey = @EmployeeClassificationKey;";

        using (var con = OpenConnection())
            return con.QuerySingle<ReadOnlyEmployeeClassification>(sql, new { employeeClassificationKey });
    }

    public void Update(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        var sql = @"UPDATE HR.EmployeeClassification
                    SET EmployeeClassificationName = @EmployeeClassificationName, IsExempt = @IsExempt, IsEmployee = @IsEmployee
                    WHERE EmployeeClassificationKey = @EmployeeClassificationKey;";

        using (var con = OpenConnection())
            con.Execute(sql, classification);
    }
}

DbConnector

DbConnector currently does not support direct constructor mapping.

Built-in functionality, including extensions, can simply be leveraged when working with immutable objects.

public class ImmutableScenario : ScenarioBase, IImmutableScenario<ReadOnlyEmployeeClassification>
{
    public ImmutableScenario(string connectionString) : base(connectionString)
    {
    }

    public int Create(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        const string sql = @"INSERT INTO HR.EmployeeClassification (EmployeeClassificationName, IsExempt, IsEmployee)
                    OUTPUT Inserted.EmployeeClassificationKey
                    VALUES(@EmployeeClassificationName, @IsExempt, @IsEmployee);";

        return DbConnector.Scalar<int>(sql, classification).Execute();
    }

    public void Delete(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        var sql = @"DELETE HR.EmployeeClassification WHERE EmployeeClassificationKey = @EmployeeClassificationKey;";

        DbConnector.NonQuery(sql, classification).Execute();
    }

    public void DeleteByKey(int employeeClassificationKey)
    {
        var sql = @"DELETE HR.EmployeeClassification WHERE EmployeeClassificationKey = @employeeClassificationKey;";

        DbConnector.NonQuery(sql, new { employeeClassificationKey }).Execute();
    }

    public ReadOnlyEmployeeClassification? FindByName(string employeeClassificationName)
    {
        var sql = @"SELECT  ec.EmployeeClassificationKey, ec.EmployeeClassificationName, ec.IsExempt, ec.IsEmployee
                    FROM HR.EmployeeClassification ec
                    WHERE ec.EmployeeClassificationName = @employeeClassificationName;";

        //DbConnector currently does not support direct constructor mapping (v1.2.1)
        return DbConnector.ReadTo<ReadOnlyEmployeeClassification>(
            sql: sql,
            param: new { employeeClassificationName },
            onLoad: (ReadOnlyEmployeeClassification result, IDbExecutionModel em, DbDataReader odr) =>
            {
                //Leverage extension from "DbConnector.Core.Extensions"
                dynamic row = odr.SingleOrDefault(em.Token, em.JobCommand);

                if (row != null)
                    result = new ReadOnlyEmployeeClassification(row.EmployeeClassificationKey, row.EmployeeClassificationName, row.IsExempt, row.IsEmployee);

                //Alternative: Extract values from the DbDataReader manually
                //if (odr.HasRows && odr.Read())
                //{
                //    int employeeClassificationKey = odr.GetValue(nameof(ReadOnlyEmployeeClassification.EmployeeClassificationKey)) as int? ?? 0;
                //    string employeeClassificationName = odr.GetValue(nameof(ReadOnlyEmployeeClassification.EmployeeClassificationName)) as string;
                //    bool isExempt = odr.GetValue(nameof(ReadOnlyEmployeeClassification.IsExempt)) as bool? ?? false;
                //    bool isEmployee = odr.GetValue(nameof(ReadOnlyEmployeeClassification.IsEmployee)) as bool? ?? false;

                //    result = new ReadOnlyEmployeeClassification(employeeClassificationKey, employeeClassificationName, isExempt, isEmployee);

                //    if (odr.Read())//SingleOrDefault behavior
                //    {
                //        throw new InvalidOperationException("The query result has more than one result.");
                //    }
                //}

                return result;
            })
            .Execute();
    }

    public IReadOnlyList<ReadOnlyEmployeeClassification> GetAll()
    {
        var sql = @"SELECT ec.EmployeeClassificationKey, ec.EmployeeClassificationName, ec.IsExempt, ec.IsEmployee FROM HR.EmployeeClassification ec;";

        //DbConnector currently does not support direct constructor mapping (v1.2.1)
        return DbConnector.ReadTo<List<ReadOnlyEmployeeClassification>>(
            sql: sql,
            param: null,
            onLoad: (List<ReadOnlyEmployeeClassification> result, IDbExecutionModel em, DbDataReader odr) =>
            {
                result = new List<ReadOnlyEmployeeClassification>();

                //Extract values from the DbDataReader manually
                if (odr.HasRows)
                {
                    while (odr.Read())
                    {
                        if (em.Token.IsCancellationRequested)
                            return result;

                        int employeeClassificationKey = odr.GetValue(nameof(ReadOnlyEmployeeClassification.EmployeeClassificationKey)) as int? ?? 0;
                        string employeeClassificationName = odr.GetValue(nameof(ReadOnlyEmployeeClassification.EmployeeClassificationName)) as string;
                        bool isExempt = odr.GetValue(nameof(ReadOnlyEmployeeClassification.IsExempt)) as bool? ?? false;
                        bool isEmployee = odr.GetValue(nameof(ReadOnlyEmployeeClassification.IsEmployee)) as bool? ?? false;

                        result.Add(new ReadOnlyEmployeeClassification(employeeClassificationKey, employeeClassificationName, isExempt, isEmployee));
                    }
                }

                return result;
            })
            .Execute()
            .ToImmutableList();
    }

    public ReadOnlyEmployeeClassification? GetByKey(int employeeClassificationKey)
    {
        var sql = @"SELECT ec.EmployeeClassificationKey, ec.EmployeeClassificationName, ec.IsExempt, ec.IsEmployee
                    FROM HR.EmployeeClassification ec
                    WHERE ec.EmployeeClassificationKey = @employeeClassificationKey;";

        //DbConnector currently does not support direct constructor mapping (v1.2.1)
        return DbConnector.ReadTo<ReadOnlyEmployeeClassification>(
            sql: sql,
            param: new { employeeClassificationKey },
            onLoad: (ReadOnlyEmployeeClassification result, IDbExecutionModel em, DbDataReader odr) =>
            {
                //Leverage extension from "DbConnector.Core.Extensions"
                dynamic row = odr.Single(em.Token, em.JobCommand);

                if (row != null)
                    result = new ReadOnlyEmployeeClassification(row.EmployeeClassificationKey, row.EmployeeClassificationName, row.IsExempt, row.IsEmployee);

                //Alternative: Extract values from the DbDataReader manually
                //if (odr.HasRows && odr.Read())
                //{
                //    int employeeClassificationKey = odr.GetValue(nameof(ReadOnlyEmployeeClassification.EmployeeClassificationKey)) as int? ?? 0;
                //    string employeeClassificationName = odr.GetValue(nameof(ReadOnlyEmployeeClassification.EmployeeClassificationName)) as string;
                //    bool isExempt = odr.GetValue(nameof(ReadOnlyEmployeeClassification.IsExempt)) as bool? ?? false;
                //    bool isEmployee = odr.GetValue(nameof(ReadOnlyEmployeeClassification.IsEmployee)) as bool? ?? false;

                //    result = new ReadOnlyEmployeeClassification(employeeClassificationKey, employeeClassificationName, isExempt, isEmployee);

                //    if (odr.Read())//Single behavior
                //    {
                //        throw new InvalidOperationException("The query result has more than one result.");
                //    }
                //}
                //else
                //{
                //    //Single behavior
                //    throw new InvalidOperationException("The query result is empty.");
                //}

                return result;
            })
            .Execute();
    }

    public void Update(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        var sql = @"UPDATE HR.EmployeeClassification
                    SET EmployeeClassificationName = @EmployeeClassificationName, IsExempt = @IsExempt, IsEmployee = @IsEmployee
                    WHERE EmployeeClassificationKey = @EmployeeClassificationKey;";

        DbConnector.NonQuery(sql, classification).Execute();
    }
}

Entity Framework 6

Entity Framework does not directly support immutable objects. You can overcome this by using a pair of conversions between the immutable object and the mutable entity.

Objects need to be materialized client-side before being mapped to the immutable type.

public ReadOnlyEmployeeClassification(EmployeeClassification entity)
{
    if (entity == null)
        throw new ArgumentNullException(nameof(entity), $"{nameof(entity)} is null.");
    if (entity.EmployeeClassificationName == null)
        throw new ArgumentNullException(nameof(entity), $"{nameof(entity.EmployeeClassificationName)} is null.");

    EmployeeClassificationKey = entity.EmployeeClassificationKey;
    EmployeeClassificationName = entity.EmployeeClassificationName;
    IsExempt = entity.IsExempt;
    IsEmployee = entity.IsEmployee;
}
public EmployeeClassification ToEntity()
{
    return new EmployeeClassification()
    {
        EmployeeClassificationKey = EmployeeClassificationKey,
        EmployeeClassificationName = EmployeeClassificationName,
        IsExempt = IsExempt,
        IsEmployee = IsEmployee
    };
}

These conversions are used in the repository before write operations and after read operations.

public class ImmutableScenario : IImmutableScenario<ReadOnlyEmployeeClassification>
{
    private Func<OrmCookbookContext> CreateDbContext;

    public ImmutableScenario(Func<OrmCookbookContext> dBContextFactory)
    {
        CreateDbContext = dBContextFactory;
    }

    public int Create(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var context = CreateDbContext())
        {
            var temp = classification.ToEntity();
            context.EmployeeClassification.Add(temp);
            context.SaveChanges();
            return temp.EmployeeClassificationKey;
        }
    }

    public virtual void Delete(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var context = CreateDbContext())
        {
            //Find the row you wish to delete
            var temp = context.EmployeeClassification.Find(classification.EmployeeClassificationKey);
            if (temp != null)
            {
                context.EmployeeClassification.Remove(temp);
                context.SaveChanges();
            }
        }
    }

    public virtual void DeleteByKey(int employeeClassificationKey)
    {
        using (var context = CreateDbContext())
        {
            //Find the row you wish to delete
            var temp = context.EmployeeClassification.Find(employeeClassificationKey);
            if (temp != null)
            {
                context.EmployeeClassification.Remove(temp);
                context.SaveChanges();
            }
        }
    }

    public ReadOnlyEmployeeClassification? FindByName(string employeeClassificationName)
    {
        using (var context = CreateDbContext())
        {
            return context.EmployeeClassification
                .Where(ec => ec.EmployeeClassificationName == employeeClassificationName)
                .ToList() //everything below this line is client-side
                .Select(x => new ReadOnlyEmployeeClassification(x)).SingleOrDefault();
        }
    }

    public IReadOnlyList<ReadOnlyEmployeeClassification> GetAll()
    {
        using (var context = CreateDbContext())
        {
            return context.EmployeeClassification
                .ToList() //everything below this line is client-side
                .Select(x => new ReadOnlyEmployeeClassification(x)).ToImmutableArray();
        }
    }

    public ReadOnlyEmployeeClassification GetByKey(int employeeClassificationKey)
    {
        using (var context = CreateDbContext())
        {
            var temp = context.EmployeeClassification.Find(employeeClassificationKey);
            if (temp == null)
                throw new DataException($"No row was found for key {employeeClassificationKey}.");
            return new ReadOnlyEmployeeClassification(temp);
        }
    }

    public void Update(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var context = CreateDbContext())
        {
            //Get a fresh copy of the row from the database
            var temp = context.EmployeeClassification.Find(classification.EmployeeClassificationKey);
            if (temp != null)
            {
                //Copy the changed fields
                temp.EmployeeClassificationName = classification.EmployeeClassificationName;
                temp.IsEmployee = classification.IsEmployee;
                temp.IsExempt = classification.IsExempt;
                context.SaveChanges();
            }
        }
    }
}

Entity Framework Core

Entity Framework Core does not directly support immutable objects. You can overcome this by using a pair of conversions between the immutable object and the mutable entity.

public ReadOnlyEmployeeClassification(EmployeeClassification entity)
{
    if (entity == null)
        throw new ArgumentNullException(nameof(entity), $"{nameof(entity)} is null.");
    if (entity.EmployeeClassificationName == null)
        throw new ArgumentNullException(nameof(entity), $"{nameof(entity.EmployeeClassificationName)} is null.");
    if (entity.IsEmployee == null)
        throw new ArgumentNullException(nameof(entity), $"{nameof(entity.IsEmployee)} is null.");

    EmployeeClassificationKey = entity.EmployeeClassificationKey;
    EmployeeClassificationName = entity.EmployeeClassificationName;
    IsExempt = entity.IsExempt;
    IsEmployee = entity.IsEmployee.Value;
}
public EmployeeClassification ToEntity()
{
    return new EmployeeClassification()
    {
        EmployeeClassificationKey = EmployeeClassificationKey,
        EmployeeClassificationName = EmployeeClassificationName,
        IsExempt = IsExempt,
        IsEmployee = IsEmployee
    };
}

These conversions are used in the repository before write operations and after read operations.

public class ImmutableScenario : IImmutableScenario<ReadOnlyEmployeeClassification>
{
    private Func<OrmCookbookContext> CreateDbContext;

    public ImmutableScenario(Func<OrmCookbookContext> dBContextFactory)
    {
        CreateDbContext = dBContextFactory;
    }

    public int Create(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var context = CreateDbContext())
        {
            var temp = classification.ToEntity();
            context.EmployeeClassification.Add(temp);
            context.SaveChanges();
            return temp.EmployeeClassificationKey;
        }
    }

    public virtual void Delete(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var context = CreateDbContext())
        {
            //Find the row you wish to delete
            var temp = context.EmployeeClassification.Find(classification.EmployeeClassificationKey);
            if (temp != null)
            {
                context.EmployeeClassification.Remove(temp);
                context.SaveChanges();
            }
        }
    }

    public virtual void DeleteByKey(int employeeClassificationKey)
    {
        using (var context = CreateDbContext())
        {
            //Find the row you wish to delete
            var temp = context.EmployeeClassification.Find(employeeClassificationKey);
            if (temp != null)
            {
                context.EmployeeClassification.Remove(temp);
                context.SaveChanges();
            }
        }
    }

    public ReadOnlyEmployeeClassification? FindByName(string employeeClassificationName)
    {
        using (var context = CreateDbContext())
        {
            return context.EmployeeClassification
                .Where(ec => ec.EmployeeClassificationName == employeeClassificationName)
                .Select(x => new ReadOnlyEmployeeClassification(x)).SingleOrDefault();
        }
    }

    public IReadOnlyList<ReadOnlyEmployeeClassification> GetAll()
    {
        using (var context = CreateDbContext())
        {
            return context.EmployeeClassification.Select(x => new ReadOnlyEmployeeClassification(x)).ToImmutableArray();
        }
    }

    public ReadOnlyEmployeeClassification GetByKey(int employeeClassificationKey)
    {
        using (var context = CreateDbContext())
        {
            var temp = context.EmployeeClassification.Find(employeeClassificationKey);
            if (temp == null)
                throw new DataException($"No row was found for key {employeeClassificationKey}.");
            return new ReadOnlyEmployeeClassification(temp);
        }
    }

    public void Update(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var context = CreateDbContext())
        {
            //Get a fresh copy of the row from the database
            var temp = context.EmployeeClassification.Find(classification.EmployeeClassificationKey);
            if (temp != null)
            {
                //Copy the changed fields
                temp.EmployeeClassificationName = classification.EmployeeClassificationName;
                temp.IsEmployee = classification.IsEmployee;
                temp.IsExempt = classification.IsExempt;
                context.SaveChanges();
            }
        }
    }
}

LINQ to DB

LINQ to DB does not directly support immutable objects. You can overcome this by using a pair of conversions between the immutable object and the mutable entity.

public ReadOnlyEmployeeClassification(EmployeeClassification entity)
{
    if (entity == null)
        throw new ArgumentNullException(nameof(entity), $"{nameof(entity)} is null.");
    if (entity.EmployeeClassificationName == null)
        throw new ArgumentNullException(nameof(entity), $"{nameof(entity.EmployeeClassificationName)} is null.");

    EmployeeClassificationKey = entity.EmployeeClassificationKey;
    EmployeeClassificationName = entity.EmployeeClassificationName;
    IsExempt = entity.IsExempt;
    IsEmployee = entity.IsEmployee;
}
public EmployeeClassification ToEntity()
{
    return new EmployeeClassification()
    {
        EmployeeClassificationKey = EmployeeClassificationKey,
        EmployeeClassificationName = EmployeeClassificationName,
        IsExempt = IsExempt,
        IsEmployee = IsEmployee
    };
}

These conversions are used in the repository before write operations and after read operations.

public class ImmutableScenario : IImmutableScenario<ReadOnlyEmployeeClassification>
{
    public int Create(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var db = new OrmCookbook())
        {
            return db.InsertWithInt32Identity(classification.ToEntity());
        }
    }

    public virtual void Delete(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var db = new OrmCookbook())
        {
            db.EmployeeClassification
                .Where(d => d.EmployeeClassificationKey == classification.EmployeeClassificationKey)
                .Delete();
        }
    }

    public virtual void DeleteByKey(int employeeClassificationKey)
    {
        using (var db = new OrmCookbook())
        {
            db.EmployeeClassification
                .Where(d => d.EmployeeClassificationKey == employeeClassificationKey)
                .Delete();
        }
    }

    public ReadOnlyEmployeeClassification? FindByName(string employeeClassificationName)
    {
        using (var db = new OrmCookbook())
        {
            var query = from ec in db.EmployeeClassification
                        where ec.EmployeeClassificationName == employeeClassificationName
                        select ec;
            return query.Select(x => new ReadOnlyEmployeeClassification(x)).SingleOrDefault();
        }
    }

    public IReadOnlyList<ReadOnlyEmployeeClassification> GetAll()
    {
        using (var db = new OrmCookbook())
        {
            return db.EmployeeClassification
                .Select(x => new ReadOnlyEmployeeClassification(x)).ToImmutableArray();
        }
    }

    public ReadOnlyEmployeeClassification GetByKey(int employeeClassificationKey)
    {
        using (var db = new OrmCookbook())
        {
            return db.EmployeeClassification.Where(d => d.EmployeeClassificationKey == employeeClassificationKey)
                .Select(x => new ReadOnlyEmployeeClassification(x)).Single();
        }
    }

    public void Update(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var db = new OrmCookbook())
        {
            db.Update(classification.ToEntity());
        }
    }
}

LLBLGen Pro

LLBLGen Pro supports 'action' specifications on entities, e.g. an entity can only be fetched, or fetched and updated, but e.g. not deleted. An entity that's marked as 'Read' can't be updated, deleted or inserted. The scope of the recipes in this cookbook however focus on immutable data in-memory. LLBLGen Pro does not directly support these objects, You can overcome this by using a pair of conversions between the immutable object and the mutable entity.

public ReadOnlyEmployeeClassification(EmployeeClassificationEntity entity)
{
    if (entity == null)
        throw new ArgumentNullException(nameof(entity), $"{nameof(entity)} is null.");
    if (entity.EmployeeClassificationName == null)
        throw new ArgumentNullException(nameof(entity), $"{nameof(entity.EmployeeClassificationName)} is null.");

    EmployeeClassificationKey = entity.EmployeeClassificationKey;
    EmployeeClassificationName = entity.EmployeeClassificationName;
    IsExempt = entity.IsExempt;
    IsEmployee = entity.IsEmployee;
}
public EmployeeClassificationEntity ToEntity()
{
    return new EmployeeClassificationEntity()
    {
        EmployeeClassificationKey = EmployeeClassificationKey,
        EmployeeClassificationName = EmployeeClassificationName,
        IsExempt = IsExempt,
        IsEmployee = IsEmployee
    };
}

These conversions are used in the repository before write operations and after read operations.

public class ImmutableScenario : IImmutableScenario<ReadOnlyEmployeeClassification>
{
    public int Create(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var adapter = new DataAccessAdapter())
        {
            var toPersist = classification.ToEntity();
            adapter.SaveEntity(toPersist);
            return toPersist.EmployeeClassificationKey;
        }
    }

    public virtual void Delete(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        DeleteByKey(classification.EmployeeClassificationKey);
    }

    public virtual void DeleteByKey(int employeeClassificationKey)
    {
        using (var adapter = new DataAccessAdapter())
        {
            adapter.DeleteEntitiesDirectly(typeof(EmployeeClassificationEntity),
                                           new RelationPredicateBucket(EmployeeClassificationFields.EmployeeClassificationKey
                                                                                       .Equal(employeeClassificationKey)));
        }
    }

    public ReadOnlyEmployeeClassification? FindByName(string employeeClassificationName)
    {
        using (var adapter = new DataAccessAdapter())
        {
            return new LinqMetaData(adapter).EmployeeClassification
                                                .Where(ec => ec.EmployeeClassificationName == employeeClassificationName)
                                                .Select(x => new ReadOnlyEmployeeClassification(x))
                                    .SingleOrDefault();
        }
    }

    public IReadOnlyList<ReadOnlyEmployeeClassification> GetAll()
    {
        using (var adapter = new DataAccessAdapter())
        {
            return new LinqMetaData(adapter).EmployeeClassification.Select(x => new ReadOnlyEmployeeClassification(x)).ToImmutableArray();
        }
    }

    public ReadOnlyEmployeeClassification GetByKey(int employeeClassificationKey)
    {
        using (var adapter = new DataAccessAdapter())
        {
            var temp = adapter.FetchNewEntity<EmployeeClassificationEntity>(
                                            new RelationPredicateBucket(EmployeeClassificationFields.EmployeeClassificationKey
                                                                                                    .Equal(employeeClassificationKey)));
            if (temp.IsNew)
                throw new DataException($"No row was found for key {employeeClassificationKey}.");
            return new ReadOnlyEmployeeClassification(temp);
        }
    }

    public void Update(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var adapter = new DataAccessAdapter())
        {
            //Get a fresh copy of the row from the database
            var temp = adapter.FetchNewEntity<EmployeeClassificationEntity>(
                                new RelationPredicateBucket(EmployeeClassificationFields.EmployeeClassificationKey
                                                                            .Equal(classification.EmployeeClassificationKey)));
            if (!temp.IsNew)
            {
                //Copy the changed fields
                temp.EmployeeClassificationName = classification.EmployeeClassificationName;
                temp.IsEmployee = classification.IsEmployee;
                temp.IsExempt = classification.IsExempt;
                adapter.SaveEntity(temp);
            }
        }
    }
}

NHibernate

NHibernate does not directly support immutable objects. You can overcome this by using a pair of conversions between the immutable object and the mutable entity.

public ReadOnlyEmployeeClassification(EmployeeClassification entity)
{
    if (entity == null)
        throw new ArgumentNullException(nameof(entity), $"{nameof(entity)} is null.");
    if (entity.EmployeeClassificationName == null)
        throw new ArgumentNullException(nameof(entity), $"{nameof(entity.EmployeeClassificationName)} is null.");

    EmployeeClassificationKey = entity.EmployeeClassificationKey;
    EmployeeClassificationName = entity.EmployeeClassificationName;
    IsExempt = entity.IsExempt;
    IsEmployee = entity.IsEmployee;
}
public EmployeeClassification ToEntity()
{
    return new EmployeeClassification()
    {
        EmployeeClassificationKey = EmployeeClassificationKey,
        EmployeeClassificationName = EmployeeClassificationName,
        IsExempt = IsExempt,
        IsEmployee = IsEmployee
    };
}

These conversions are used in the repository before write operations and after read operations.

public class ImmutableScenario : IImmutableScenario<ReadOnlyEmployeeClassification>
{
    readonly ISessionFactory m_SessionFactory;

    public ImmutableScenario(ISessionFactory sessionFactory)
    {
        m_SessionFactory = sessionFactory;
    }

    public int Create(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var session = m_SessionFactory.OpenSession())
        {
            var temp = classification.ToEntity();
            session.Save(temp);
            session.Flush();
            return temp.EmployeeClassificationKey;
        }
    }

    public void Delete(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var session = m_SessionFactory.OpenSession())
        {
            session.Delete(classification.ToEntity());
            session.Flush();
        }
    }

    public ReadOnlyEmployeeClassification? FindByName(string employeeClassificationName)
    {
        using (var session = m_SessionFactory.OpenStatelessSession())
        {
            return session.QueryOver<EmployeeClassification>().Where(e => e.EmployeeClassificationName == employeeClassificationName).List()
                .Select(x => new ReadOnlyEmployeeClassification(x)).SingleOrDefault();
        }
    }

    public IReadOnlyList<ReadOnlyEmployeeClassification> GetAll()
    {
        using (var session = m_SessionFactory.OpenStatelessSession())
        {
            return session.QueryOver<EmployeeClassification>().List()
                .Select(x => new ReadOnlyEmployeeClassification(x)).ToImmutableArray();
        }
    }

    public ReadOnlyEmployeeClassification? GetByKey(int employeeClassificationKey)
    {
        using (var session = m_SessionFactory.OpenStatelessSession())
        {
            var result = session.Get<EmployeeClassification>(employeeClassificationKey);
            return new ReadOnlyEmployeeClassification(result);
        }
    }

    public void Update(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var session = m_SessionFactory.OpenSession())
        {
            session.Update(classification.ToEntity());
            session.Flush();
        }
    }
}

RepoDb

RepoDb does not directly support immutable objects. You have to manage the conversion between mutable and immutable objects in order to make it work.

Below is a sample snippet for immutable class.

[Map("[HR].[EmployeeClassification]")]
public class ReadOnlyEmployeeClassification : IReadOnlyEmployeeClassification
{
    public ReadOnlyEmployeeClassification(EmployeeClassification classification)
    {
        if (classification?.EmployeeClassificationName == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification.EmployeeClassificationName)} is null.");

        EmployeeClassificationKey = classification.EmployeeClassificationKey;
        EmployeeClassificationName = classification.EmployeeClassificationName;
        IsExempt = classification.IsExempt;
        IsEmployee = classification.IsEmployee;
    }

    public ReadOnlyEmployeeClassification(int employeeClassificationKey,
        string employeeClassificationName,
        bool isExempt,
        bool isEmployee)
    {
        EmployeeClassificationKey = employeeClassificationKey;
        EmployeeClassificationName = employeeClassificationName;
        IsExempt = isExempt;
        IsEmployee = isEmployee;
    }

    public int EmployeeClassificationKey { get; }

    public string EmployeeClassificationName { get; }
    public bool IsEmployee { get; }
    public bool IsExempt { get; }
}

Below is a sample snippet for mutable class.

[Map("[HR].[EmployeeClassification]")]
public class EmployeeClassification : IEmployeeClassification
{
    public EmployeeClassification()
    {
    }

    public EmployeeClassification(IReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        EmployeeClassificationKey = classification.EmployeeClassificationKey;
        EmployeeClassificationName = classification.EmployeeClassificationName;
        IsExempt = classification.IsExempt;
        IsEmployee = classification.IsEmployee;
    }

    public int EmployeeClassificationKey { get; set; }

    public string? EmployeeClassificationName { get; set; }

    public bool IsEmployee { get; set; }

    public bool IsExempt { get; set; }

    internal ReadOnlyEmployeeClassification ToImmutable()
    {
        return new ReadOnlyEmployeeClassification(this);
    }
}

Below is the immutable repository.

public class ImmutableScenario : BaseRepository<ReadOnlyEmployeeClassification, SqlConnection>,
    IImmutableScenario<ReadOnlyEmployeeClassification>
{
    public ImmutableScenario(string connectionString)
        : base(connectionString, RDB.Enumerations.ConnectionPersistency.Instance)
    { }

    public int Create(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        return Insert<int>(classification);
    }

    public void Delete(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        base.Delete(classification);
    }

    public void DeleteByKey(int employeeClassificationKey)
    {
        Delete(employeeClassificationKey);
    }

    public ReadOnlyEmployeeClassification? FindByName(string employeeClassificationName)
    {
        return Query(e => e.EmployeeClassificationName == employeeClassificationName)
            .FirstOrDefault();
    }

    public IReadOnlyList<ReadOnlyEmployeeClassification> GetAll()
    {
        return QueryAll()
            .ToImmutableList();
    }

    public ReadOnlyEmployeeClassification? GetByKey(int employeeClassificationKey)
    {
        return Query(employeeClassificationKey)
            .FirstOrDefault();
    }

    public void Update(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        base.Update(classification);
    }
}

ServiceStack

public class ImmutableScenario : IImmutableScenario<ReadOnlyEmployeeClassification>
{
    private readonly IDbConnectionFactory _dbConnectionFactory;

    public ImmutableScenario(IDbConnectionFactory dbConnectionFactory)
    {
        _dbConnectionFactory = dbConnectionFactory;
    }

    public int Create(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var db = _dbConnectionFactory.OpenDbConnection())
        {
            return (int)db.Insert(new EmployeeClassification().PopulateWith(classification), true);
        }
    }

    public void Delete(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var db = _dbConnectionFactory.OpenDbConnection())
        {
            var deleted = db.Delete<EmployeeClassification>(r => r.Id == classification.Id);
            if (deleted != 1)
                throw new DataException($"No row was found for key {classification.EmployeeClassificationKey}.");
        }
    }

    public void DeleteByKey(int employeeClassificationKey)
    {
        using (var db = _dbConnectionFactory.OpenDbConnection())
        {
            var deleted = db.Delete<EmployeeClassification>(r => r.Id == employeeClassificationKey);
            if (deleted != 1)
                throw new DataException($"No row was found for key {employeeClassificationKey}.");
        }
    }

    public ReadOnlyEmployeeClassification? FindByName(string employeeClassificationName)
    {
        using (var db = _dbConnectionFactory.OpenDbConnection())
        {
            var temp = db.Single<EmployeeClassification>(r =>
                r.EmployeeClassificationName == employeeClassificationName);
            return temp == null ? null : new ReadOnlyEmployeeClassification(temp);
        }
    }

    public IReadOnlyList<ReadOnlyEmployeeClassification> GetAll()
    {
        using (var db = _dbConnectionFactory.OpenDbConnection())
        {
            return db.Select<EmployeeClassification>()
                .Select(x => new ReadOnlyEmployeeClassification(x))
                .ToImmutableArray();
        }
    }

    public ReadOnlyEmployeeClassification? GetByKey(int employeeClassificationKey)
    {
        using (var db = _dbConnectionFactory.OpenDbConnection())
        {
            var temp = db.Single<EmployeeClassification>(r =>
                r.Id == employeeClassificationKey);
            if (temp == null)
                throw new DataException($"No row was found for key {employeeClassificationKey}.");
            return new ReadOnlyEmployeeClassification(temp);
        }
    }

    public void Update(ReadOnlyEmployeeClassification classification)
    {
        if (classification == null)
            throw new ArgumentNullException(nameof(classification), $"{nameof(classification)} is null.");

        using (var db = _dbConnectionFactory.OpenDbConnection())
        {
            db.Update<EmployeeClassification>(new
            {
                classification.IsEmployee,
                classification.IsExempt,
                classification.EmployeeClassificationName
            }, r => r.Id == classification.Id);
        }
    }
}