3
\$\begingroup\$

I have the following situation:
I have...

  • Models like ServerRoom and ServerCabinet
  • Repositories of the Model instances (in-memory collection)
  • PersistenceServices which are interfacing the persistence-method I chose (mssql db)

The problem I am facing is repetition.
Basically, the implementation of a PersistenceService is not really bound to a specific Model, only to it's instantiation (from a DataRow) and conversion (from Business Object to DataRow), so I thought the following would be good:

From

/// <summary>
/// Implements generic database-crud operations to prevent repetition
/// Most of the CRUD-operations are the exactly the same, and those are implemented
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class DbPersistenceService<T> : IPersistenceService<T>
    where T : class
{
    protected DataTable _dataTable;

    // Update, Delete, GetAll, Save, Delete, Contains and FindRow excluded

    /// <summary>
    /// Add an instance to the repository
    /// </summary>
    /// <param name="instance"></param>
    public void Add(T instance)
    {
        _dataTable.Rows.Add(ObjectToRow(instance));
    }

    /// <summary>
    /// Get the instance with the specified id
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public T GetById(long id)
    {
        var row = FindRow(id);
        if (row == null)
            return null;
        return RowToObject(FindRow(id));
    }

    /// <summary>
    /// Converts a datarow to the domain object
    /// </summary>
    /// <param name="row"></param>
    /// <returns></returns>
    protected abstract T RowToObject(DataRow row);

    /// <summary>
    /// Converts a domain object to a datarow
    /// </summary>
    /// <param name="instance"></param>
    /// <returns></returns>
    protected abstract DataRow ObjectToRow(T instance);

}

So, the CRUD operations are the same for each model, only the conversion between domain and persistence layer is different (constructor of ServerCabinet needs other information from a DataRow than a ServerRoom) and this conversion is must be implemented by overriding RowToObject and ObjectToRow.
I am not satisfied with this design, as I still have to maintain one PersistenceService-class for each Model (e.g. ServerCabinetDbPersistenceService). I tried things like an interface IBusinessDataConverter, but it doesnt work, because that would be instance-methods whereas methods like GetById (above) cannot access an instance, only static, class-level methods.

How would you design it, do you have other general info about bad-practices I used?


          /* Using an ORM like EntityFramework is not an option */
\$\endgroup\$
  • \$\begingroup\$ "but it doesnt work, because that would be instance-methods whereas methods like GetById (above) cannot access an instance, only static, class-level methods." I don't really understand what you mean here \$\endgroup\$ – Ben Aaronson Oct 24 '16 at 10:38
  • \$\begingroup\$ Try (or inspire from) a micro ORM, it might to already what you need. \$\endgroup\$ – Adrian Iftode Oct 24 '16 at 10:51
1
\$\begingroup\$

I would tend to agree with Adrian's comment that you might be better off finding some lightweight / micro ORM to start from, if bigger things such as EF or NHibernate aren't options for you.

Still, if you really want to go all the way long your own route with generics, then, FWIW, I'd probably put it along the lines of something like:

(just a rough, compilable sketch; untested, clearly not thread-safe, with no provisions or hints on how to go from the DataTables to your DB engine's SQL, etc)

public interface IStore<TItem>
{
    TPersistent Deserialize<TPersistent>(TItem item)
        where TPersistent : class, new();

    TItem Serialize<TPersistent>(TPersistent instance)
        //if only for consistency with Deserialize:
        where TPersistent : class, new();
}

public interface IPersistence<TStore, TItem>
    where TStore : IStore<TItem>
{
    void Add<TPersistent>(TPersistent instance)
        //if only for consistency with GetById, or other various
        //Getters/Finders/Selectors:
        where TPersistent : class, new();

    TPersistent GetById<TPersistent>(long id)
        where TPersistent : class, new();
}

public abstract class Persistence<TStore, TItem> : IPersistence<TStore, TItem>
    where TStore : IStore<TItem>
{
    protected abstract TItem Get<TPersistent>(long id);

    protected abstract void Insert(TItem item);

    // Useful to use TStore here (and not just IStore<TItem>) to allow for
    // type-safe internal friendship between the store and persistence implementations later on,
    // if needed
    protected TStore Store { get; private set; }

    protected Persistence(IStore<TItem> store)
    {
        // Note we *want* this to barf at construction time,
        // if it turns out that store (the parameter)
        // does not conform to TStore
        Store = (TStore)store;
    }

    void IPersistence<TStore, TItem>.Add<TPersistent>(TPersistent instance)
    {
        Insert(Store.Serialize(instance));
    }

    TPersistent IPersistence<TStore, TItem>.GetById<TPersistent>(long id)
    {
        return Store.Deserialize<TPersistent>(Get<TPersistent>(id));
    }
}

/// <summary>
/// Friend of DataTablePersistence (via its internal DataSet)
/// </summary>
public class DataTableStore : IStore<DataRow>
{
    internal readonly DataSet DataSet;

    internal DataTable EnsureTable(Type instanceType)
    {
        var tableName = instanceType.Name;
        DataTable table;
        if (!DataSet.Tables.Contains(tableName))
        {
            table = new DataTable(tableName);
            //(left to-do) also define the table schema (columns),
            //after what instanceType tells us
            DataSet.Tables.Add(tableName);
        }
        else
        {
            table = DataSet.Tables[tableName];
        }
        return table;
    }

    public DataTableStore()
    {
        DataSet = new DataSet();
    }

    TPersistent IStore<DataRow>.Deserialize<TPersistent>(DataRow item)
    {
        var instance = new TPersistent();
        //e.g., use System.Reflection to hydrate instance (from item)
        return instance;
    }

    DataRow IStore<DataRow>.Serialize<TPersistent>(TPersistent instance)
    {
        var table = EnsureTable(instance.GetType());
        var item = table.NewRow();
        //e.g., use System.Reflection to dehydrate instance (into item)
        return item;
    }
}

/// <summary>
/// Friend of DataTableStore (via DataTableStore's internal DataSet)
/// </summary>
public class DataTablePersistence : Persistence<DataTableStore, DataRow>
{
    protected override DataRow Get<TPersistent>(long id)
    {
        var table = Store.EnsureTable(typeof(TPersistent));
        return table.Rows.Find(id);
    }

    protected override void Insert(DataRow item)
    {
        // presumably, item was created by some DataSet[<table name>].NewRow(),
        // so, it already knows the table in which it ought to be inserted
        item.Table.Rows.Add(item);
    }

    public DataTablePersistence()
        : base(new DataTableStore())
    {
    }
}

public class Program
{
    public class Person
    {
        public long Id { get; set; }

        public string First { get; set; }

        public string Last { get; set; }
    }

    public class Asset
    {
        public long Id { get; set; }

        public string Category { get; set; }

        public string Name { get; set; }
    }

    public static void Main(string[] args)
    {
        IPersistence<DataTableStore, DataRow> persistence = new DataTablePersistence();

        persistence.Add(new Person { First = "John", Last = "Smith" });

        persistence.Add(new Asset { Category = "Vehicle", Name = "Cadillac" });

        var john = persistence.GetById<Person>(1);

        var asset = persistence.GetById<Asset>(1);

        //etc, etc
    }
}

Finally, it'll get more interesting (challenging) if you want to support dehydrating (i.e., emitting SQL insert/update/delete) arbitrary object graphs where some instances are transient/freshly created, while others come with their existing keys from your underlying DB, etc)

'Hope this helps.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.