5
\$\begingroup\$

For a given Entity Framework data set, I have written several adapters to import data into my entity models. Rather than manually write mapping code for each entity model, I tried for a generalized approach using reflection.

My requirements are:

  • The updater must create the entity if it does not exist, or update the existing entity with any changed properties or relationships.
  • External data are often subsets; therefore, do not delete entities if they are not in external data
  • If external data reference other entities which do not exist, eg tags, the updater must create them and ensure the references are still valid, while respecting key constraints.

My entities implement an interface, IEntity:

public interface IEntity
{
    int Id { get; set; }
    T CreateEmpty<T>() where T:class,IEntity;
    bool EntityEquals(IEntity entity);
}

EntityEquals compares entities based on their natural keys. An implementation might look like:

public class Tag : IEntity
{
    public int Id { get; set; }
    // Name is a natural key
    public string Name { get; set; }

    public T CreateEmpty<T>() where T : class, IEntity
    {
        return new Tag() as T;
    }

    public bool EntityEquals(IEntity entity)
    {
        var e = entity as Tag;
        return e != null && Name==e.Name;
    }
}

I invoke this like

EntityDataUpdater.Update(context, externalEntitiesList);

And here's the implementation. Comments and feedback greatly appreciated!

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using Project.Models;

namespace Project.Models.Adapters
{
    public static class EntityDataUpdater
    {

        public static void Update(DbContext context, IEnumerable<IEntity> entities)
        {
            if (!entities.Any()) return;

            var set = context.Set(entities.First().GetType());
            set.Load(); // prefetch data to memory

            foreach (var entity in entities)
            {
                var tEntity = entity.ImportOrUpdate(context);

                // if newly imported, add it
                if (tEntity.Id == 0) set.Add(tEntity);
                // otherwise, indicate that it is updated
                else context.Entry(tEntity).State = EntityState.Modified;
            }

            context.SaveChanges();
        }
    }

    static class EntityCopyExtensions
    {
        private static bool TryFindIn<T>(this T entity, DbContext context, out T o) where T : class, IEntity
        {
            var set = context.Set(entity.GetType());

            o = set.Local.Cast<IEntity>()
                .FirstOrDefault(item => item.EntityEquals(entity)) as T // check local first
                ?? ((IEnumerable)set).Cast<IEntity>()
                .FirstOrDefault(item => item.EntityEquals(entity)) as T; // then check DB
            return o != null;
        }

        private static IEntity FindOrCreateIn(this IEntity entityIn, DbContext context)
        {
            // if item exists, return it; otherwise create it
            IEntity entityOut;
            return !entityIn.TryFindIn(context, out entityOut) ? entityIn.ImportOrUpdate(context) : entityOut;
        }

        public static T ImportOrUpdate<T>(this T entityIn, DbContext context) where T : class, IEntity
        {
            var entityT = entityIn.GetType();

            T entityOut;
            if(!entityIn.TryFindIn(context, out entityOut))
            {
                entityOut = entityIn.CreateEmpty<T>();
            }

            // copy properties
            foreach (var property in entityT.GetProperties()
                        .Where(p => p.Name != "Id" &&
                            p.CanWrite && (
                            p.PropertyType == typeof(string) ||
                            p.PropertyType == typeof(int) ||
                            p.PropertyType == typeof(DateTime)
                        ))){
                var value = property.GetValue(entityIn, null);
                if (value != null) property.SetValue(entityOut, value, null);
            }

            // copy direct references
            foreach (var reference in entityT.GetProperties()
                        .Where(r => r.PropertyType.IsImplementationOf(typeof(IEntity)))
                    ){
                var origValue = (IEntity) reference.GetValue(entityIn, null);
                reference.SetValue(entityOut,origValue.FindOrCreateIn(context),null);
            }

            // copy collections of references
            foreach (var reference in entityT.GetProperties()
                .Where(r=>
                    r.PropertyType.IsImplementationOf(typeof(IEnumerable)) && 
                    r.PropertyType.IsGenericType &&
                    r.PropertyType.GetGenericArguments().First().IsImplementationOf(typeof(IEntity))
                )){

                var itemsIn = (IEnumerable) (reference.GetValue(entityIn, null));

                // itemsOut begins with all items from entityOut
                var itemsOut = ((IEnumerable) (reference.GetValue(entityOut, null))).Cast<IEntity>().ToList();

                // then adds any new items from entityIn
                foreach (IEntity item in itemsIn)
                {
                    var itemOut = item.FindOrCreateIn(context);
                    //only add new items
                    if (!itemsOut.Any(i => i.Id == itemOut.Id) || item.Id==0)
                    {
                        itemsOut.Add(item.FindOrCreateIn(context));
                    }
                }

                reference.SetValue(entityOut,itemsOut.CastTo(reference.PropertyType),null);
            }

            return entityOut;
        }

        /// <summary>
        /// Cast each element in <paramref name="list"/> to type of the first object,
        /// or null
        /// </summary>
        private static IEnumerable CastTo(this IList list, Type targetT)
        {
            if (!(targetT.IsImplementationOf(typeof(IEnumerable)) && targetT.IsGenericType)) 
                throw new ArgumentException("targetT must be an IEnumerable of T.");

            var itemT = targetT.GetGenericArguments().First();

            var ret = (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(new[] {itemT}));
            foreach (var i in list)
                ret.Add(i);

            return ret;
        }

        /// <summary>
        /// Returns true if type <paramref name="t"/> implements interface <paramref name="i"/>
        /// </summary>
        private static bool IsImplementationOf(this Type t, Type i)
        {
            if (!i.IsInterface) throw new ApplicationException("Method must be of type Interface");
            return t.GetInterfaces().Contains(i);
        }
        /// <summary>
        /// Returns true if type <paramref name="t"/> implements interface <paramref name="i"/>
        /// </summary>
        private static bool IsImplementationOf(this Type t, string i)
        {
            if (!Type.GetType(i, true).IsInterface) throw new ApplicationException("Method must be of type Interface");
            return t.GetInterface(i) != null;
        }    
    }
}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I personally don't like exposing Ids as their base data type (in your case, int) for a couple of reasons:

  1. Somewhere, in client code, someone can assign an int of one type of entity to another by mistake and nothing will stop you (i.e. no strong typing). This can wreak havoc in the DB.

  2. If the underlying data type ever needs to change (e.g. you change databases or figure your int now needs to be a long/bigint), you have to find all places where you reference it as an int and change it. Encapsulating the Id keeps it localized to the DAL.

So I tend to wrap that up thus (the Origin property I normally leave empty, but it can represent different schemas in a database where you may have entity name overlap, etc.):

/// <summary>
/// Defines an interface for an object's unique key in order to abstract out the underlying key
/// generation/maintenance mechanism.
/// </summary>
/// <typeparam name="T">The type the key is representing.</typeparam>
public interface IModelId<T> where T : class, IEntity<T>
{
    /// <summary>
    /// Gets a string representation of the domain the model originated from.
    /// </summary>
    /// <value>The origin.</value>
    string Origin
    {
        get;
    }

    /// <summary>
    /// The model instance identifier for the model object that this <see cref="IModelId{T}"/> refers to.
    /// Typically, this is a database key, file name, or some other unique identifier.
    /// <typeparam name="TKeyDataType">The expected data type of the identifier.</typeparam>
    /// </summary>
    /// <typeparam name="TKeyDataType">The expected data type of the identifier.</typeparam>
    /// <returns>The unique key as the data type specified.</returns>
    TKeyDataType GetKey<TKeyDataType>();

    /// <summary>
    /// Performs an equality check on the two model identifiers and returns <c>true</c> if they are equal; otherwise
    /// <c>false</c> is returned.  All implementations must also override the equal operator.
    /// </summary>
    /// <param name="obj">The identifier to compare against.</param>
    /// <returns><c>true</c> if the identifiers are equal; otherwise <c>false</c> is returned.</returns>
    bool Equals(IModelId<T> obj);
}

I have a base class handle the Origin property for me:

/// <summary>
/// Represents an object's unique key in order to abstract out the underlying key generation/maintenance mechanism.
/// </summary>
/// <typeparam name="T">The type the key is representing.</typeparam>
public abstract class ModelIdBase<T> : IModelId<T> where T : class, IEntity<T>
{
    /// <summary>
    /// Gets a string representation of the domain the model originated from.
    /// </summary>
    public string Origin
    {
        get;

        internal set;
    }

    /// <summary>
    /// The model instance identifier for the model object that this <see cref="ModelIdBase{T}"/> refers to.
    /// Typically, this is a database key, file name, or some other unique identifier.
    /// </summary>
    /// <typeparam name="TKeyDataType">The expected data type of the identifier.</typeparam>
    /// <returns>The unique key as the data type specified.</returns>
    public abstract TKeyDataType GetKey<TKeyDataType>();

    /// <summary>
    /// Performs an equality check on the two model identifiers and returns <c>true</c> if they are equal;
    /// otherwise <c>false</c> is returned. All implementations must also override the equal operator.
    /// </summary>
    /// <param name="obj">The identifier to compare against.</param>
    /// <returns>
    ///   <c>true</c> if the identifiers are equal; otherwise <c>false</c> is returned.
    /// </returns>
    public abstract bool Equals(IModelId<T> obj);
}

Finally, I have a couple implementations I use currently - one for int and another for Guid. Here's the int implementation:

/// <summary>
/// Represents an abstraction of the database key for a Model Identifier.
/// </summary>
/// <typeparam name="T">The expected owner data type for this identifier.</typeparam>
[DebuggerDisplay("Origin={Origin}, Integer Identifier={id}")]
public sealed class IntId<T> : ModelIdBase<T> where T : class, IEntity<T>
{
    /// <summary>
    /// Gets or sets the unique ID.
    /// </summary>
    /// <value>The unique ID.</value>
    internal int Id
    {
        get;

        set;
    }

    /// <summary>
    /// Implements the operator ==.
    /// </summary>
    /// <param name="intIdentifier1">The first Model Identifier to compare.</param>
    /// <param name="intIdentifier2">The second Model Identifier to compare.</param>
    /// <returns>
    ///   <c>true</c> if the instances are equal; otherwise <c>false</c> is returned.
    /// </returns>
    public static bool operator ==(IntId<T> intIdentifier1, IntId<T> intIdentifier2)
    {
        return object.Equals(intIdentifier1, intIdentifier2);
    }

    /// <summary>
    /// Implements the operator !=.
    /// </summary>
    /// <param name="intIdentifier1">The first Model Identifier to compare.</param>
    /// <param name="intIdentifier2">The second Model Identifier to compare.</param>
    /// <returns>
    ///   <c>true</c> if the instances are equal; otherwise <c>false</c> is returned.
    /// </returns>
    public static bool operator !=(IntId<T> intIdentifier1, IntId<T> intIdentifier2)
    {
        return !object.Equals(intIdentifier1, intIdentifier2);
    }

    /// <summary>
    /// Performs an implicit conversion from <see cref="IntId{T}"/> to <see cref="System.Int32"/>.
    /// </summary>
    /// <param name="id">The identifier.</param>
    /// <returns>The result of the conversion.</returns>
    public static implicit operator int(IntId<T> id)
    {
        return id == null ? int.MinValue : id.GetKey<int>();
    }

    /// <summary>
    /// Performs an implicit conversion from <see cref="System.Int32"/> to <see cref="IntId{T}"/>.
    /// </summary>
    /// <param name="id">The identifier.</param>
    /// <returns>The result of the conversion.</returns>
    public static implicit operator IntId<T>(int id)
    {
        return new IntId<T> { Id = id };
    }

    /// <summary>
    /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current
    /// <see cref="T:System.Object"/>.
    /// </summary>
    /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current
    /// <see cref="T:System.Object"/>.</param>
    /// <returns>true if the specified <see cref="T:System.Object"/> is equal to the current
    /// <see cref="T:System.Object"/>; otherwise, false.</returns>
    /// <exception cref="T:System.NullReferenceException">The <paramref name="obj"/> parameter is null.</exception>
    public override bool Equals(object obj)
    {
        return this.Equals(obj as IModelId<T>);
    }

    /// <summary>
    /// Serves as a hash function for a particular type.
    /// </summary>
    /// <returns>
    /// A hash code for the current <see cref="T:System.Object"/>.
    /// </returns>
    public override int GetHashCode()
    {
        unchecked
        {
            var hash = 17;

            hash = (23 * hash) + (this.Origin == null ? 0 : this.Origin.GetHashCode());
            return (31 * hash) + this.GetKey<int>().GetHashCode();
        }
    }

    /// <summary>
    /// Returns a <see cref="System.String"/> that represents this instance.
    /// </summary>
    /// <returns>
    /// A <see cref="System.String"/> that represents this instance.
    /// </returns>
    public override string ToString()
    {
        return this.Origin + ":" + this.GetKey<int>().ToString(CultureInfo.InvariantCulture);
    }

    /// <summary>
    /// Performs an equality check on the two model identifiers and returns <c>true</c> if they are equal;
    /// otherwise <c>false</c> is returned.  All implementations must also override the equal operator.
    /// </summary>
    /// <param name="obj">The identifier to compare against.</param>
    /// <returns>
    ///   <c>true</c> if the identifiers are equal; otherwise <c>false</c> is returned.
    /// </returns>
    public override bool Equals(IModelId<T> obj)
    {
        if (obj == null)
        {
            return false;
        }

        return (obj.Origin == this.Origin) && (obj.GetKey<int>() == this.GetKey<int>());
    }

    /// <summary>
    /// Generates an object from its string representation.
    /// </summary>
    /// <param name="value">The value of the model's type.</param>
    /// <returns>A new instance of this class as it's interface containing the value from the string.</returns>
    internal static ModelIdBase<T> FromString(string value)
    {
        if (value == null)
        {
            throw new ArgumentNullException("value");
        }

        int id;
        var originAndId = value.Split(new[] { ":" }, StringSplitOptions.None);

        if (originAndId.Length != 2)
        {
            throw new ArgumentOutOfRangeException("value", "value must be in the format of Origin:Identifier");
        }

        return int.TryParse(originAndId[1], NumberStyles.None, CultureInfo.InvariantCulture, out id)
            ? new IntId<T> { Id = id, Origin = originAndId[0] }
            : null;
    }

    /// <summary>
    /// The model instance identifier for the model object that this <see cref="ModelIdBase{T}"/> refers to.
    /// Typically, this is a database key, file name, or some other unique identifier.
    /// </summary>
    /// <typeparam name="TKeyDataType">The expected data type of the identifier.</typeparam>
    /// <returns>The unique key as the data type specified.</returns>
    public override TKeyDataType GetKey<TKeyDataType>()
    {
        return (TKeyDataType)Convert.ChangeType(this.Id, typeof(TKeyDataType), CultureInfo.InvariantCulture);
    }
}

Your interface for entities would then be defined as:

public interface IEntity<T> where T : class, IEntity<T>
{
    IModelId<T> Id { get; set; }
    U CreateEmpty<U>() where U : class,IEntity<T>;
    bool EntityEquals(IEntity<T> entity);
}

Your DAL would be able to set the Id property as such (using the Tag class):

public class Tag : IEntity<Tag>
{
    public IModelId<Tag> Id { get; set; }
    // Name is a natural key
    public string Name { get; set; }

    public T CreateEmpty<T>() where T : class, IEntity<Tag>
    {
        return new Tag() as T;
    }

    public bool EntityEquals(IEntity<Tag> entity)
    {
        var e = entity as Tag;
        return e != null && Name == e.Name;
    }
}

The setting code (obviously not real):

Tag tag = new Tag();

tag.Id = new IntId<Tag> { Id = 42 };
Console.WriteLine(tag.Id.GetKey<int>());

Your helper classes would need some rework as well to handle that. Now, all this being said and done, I'm not sure how Entity Framework would jibe with this notion without a bunch of help.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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