Improving the Disconnected Repository using LINQ to SQL


I have worked through the problem a bit further. It is still a bit nasty with the generics, but it works better. And I can now use any type of primary key. If you need to get an understanding of the entire process, look at my last entry.

In order to figure out the primary key for a LINQ to SQL object, you have to query properties of the object through reflection. Here is a generic method that will return the primary key. Note that all of my tables contain primary keys, so you have to work with nulls if you do not. I am also only using integers for my primary keys, so I optimistically assume an integer for the object. I still end up with one unbox in the mix, but it works fairly well.

To get the primary key, you first get the type. You then get every PropertyInfo on the type and finally check each attribute on each property. It goes like so:

private string GetPrimaryKeyName(T entity)
{
    Type type = entity.GetType();
    foreach(PropertyInfo prop in type.GetProperties())
    {
        object[] attributes = prop.GetCustomAttributes(true);

        foreach(object o in attributes)
        {
            ColumnAttribute attribute = (ColumnAttribute) o;
            string name = o.ToString();

            if (attribute.IsPrimaryKey)
                return prop.Name;
        }
    }

    return null;
}

The null is thrown in to make the routine compile. I could have done the same by doing the following:

private string GetPrimaryKeyName(T entity)
{
   
Type type = entity.GetType();
   
string returnVal = null;

    foreach (PropertyInfo prop in type.GetProperties())
    {
       
object[] attributes = prop.GetCustomAttributes(true);

        foreach (object o in attributes)
        {
           
ColumnAttribute attribute = (ColumnAttribute)o;
           
string name = o.ToString();

            if (attribute.IsPrimaryKey)
            {
               
returnVal = prop.Name;
               
break;

            }
        }
    }

    return returnVal;}

I am not as fond of this syntax, but it will accomplish the same thing. As long as you have a primary key on the table, the LINQ to SQL object will have the following type of attribute:

[Column(Storage="_SimId", AutoSync=AutoSync.OnInsert, DbType="Int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)]

I can then get the value of the primary key, as an object, using the following routine.

private object GetIdValue(T entity, string idName)
{
    Type type = entity.GetType();
    PropertyInfo pi = type.GetProperty(idName);
    MethodInfo mi = pi.GetGetMethod();
    return mi.Invoke(entity, null);
}

To invoke this, I use the name I have pulled from the GetPrimaryKeyName() routine. This reduces down my main routine from this:

Type t1 = entity.GetType();
//This is a dangerous assumption
string idName = t1.Name + "Id";
PropertyInfo pi = t1.GetProperty(idName);
MethodInfo mi = pi.GetGetMethod();
object id = mi.Invoke(entity, null);

To this:

string idName = GetPrimaryKeyName(entity);
int id = (int)GetIdValue(entity, idName);

 

I find that much cleaner and more likely to show my intent. In addition, the primary key is no longer constrained to {TableName} + "Id". This is much cleaner. I can now offload the loading of the database entity object, as well. Here is my routine.

private void LoadDatabaseEntity(T entity, ref T databaseEntity, string idName)
{
    Type t1 = entity.GetType();
    Type t2 = databaseEntity.GetType();

    foreach (PropertyInfo prop in t1.GetProperties())
    {
        bool containsName = prop.Name.Contains(t1.Name);
        bool containsId = prop.Name.ToLower().Contains("id");

        if (prop.Name.ToLower() != idName)
        {
            PropertyInfo prop2 = t2.GetProperty(prop.Name);

            //How to get property value
            MethodInfo m1 = prop.GetGetMethod();
            object o = m1.Invoke(entity, null);

            MethodInfo m2 = prop2.GetSetMethod();
            m2.Invoke(databaseEntity, new object[] { o });
        }
    }
}

There is still one thing I would like to refactor and that is sending in the databaseEntity object by reference. It feels a lot like cheating, but with the way the LINQ context works, I will have to play around with it a bit prior to moving away from by reference. I am still loading each property. If I were building this completely from scratch, I might look into a way to flag each field as dirty to avoid setting each field, but this is not extremely dangerous as long as I do not do anything stupid prior to attempting to save an object. There is that danger, of course, and the generic implementation makes it difficult to add safety for everything (like violating the foreign key constraint). I am willing to take this risk, as I have the need to expose these objects to another language. I will check input when someone uses the web service to avoid danger. A bit more weight on the web service call, but I am not writing for Google scale here.

Here is the entire Save method, with the calls to the methods above:

public T Save(T entity)
{
    using (System.Data.Linq.DataContext context = _dataContextFactory.Context)
    {
        T databaseEntity;
        string idName = GetPrimaryKeyName(entity);
        int id = (int)GetIdValue(entity, idName);

        //Then
        if(0 == id)
        {
            //This is a save
            return Insert(entity);
        }
        else
        {
            databaseEntity = context.GetTable<T>().First(s => s == entity);
            LoadDatabaseEntity(entity, ref databaseEntity, idName);
        }

        context.SubmitChanges();
        return databaseEntity;
    }
}

There are a few tradeoffs here, since the implementation is generic. For those who don’t like searching the properties through reflection, you can create your own specific repository for the Save() method. I am still leaving the Insert() method separate, for times when I know I am just inserting, but I could just as easily refactor to this:

                if(0 == id)
                {
                    //This is a new object
                    context.GetTable<T>().InsertOnSubmit(entity);
                    context.SubmitChanges();
                    return entity;
                }

I am going to think about this, as I am not sure Insert() by itself serves any purpose. Since I have already gotten a query about showing my entire class, I will say that this request is forthcoming. I want to make sure I have the best way of doing the things I am doing here. Here are a couple of things I am going to look at tonight:

More efficient way of returning the primary key

Some way to properly handle all primary key types

Peace and Grace,
Greg

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: