Click here to Skip to main content
15,895,557 members
Articles / Web Development / XHTML

An ASP.NET Data Layer Base Class for LINQ Disconnected Mode

Rate me:
Please Sign up or sign in to vote.
4.73/5 (21 votes)
14 Mar 2009CPOL6 min read 81.3K   1.1K   68  
Quickly and easily implement your LINQ Data Layer with this abstract class
using System;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Vandermotten.Diagnostics;


namespace DeverMind
{

    /// <summary>
    /// Abstract Linq repository base class written by Adrian Grigore
    /// 
    /// RepositoryBase supports disconnected mode and recursive saving / deleting of linq entities
    /// 
    /// For details about this class, updated versions, and more useful tips please visit
    /// http://devermind.com/linq/a-linq-disconnected-mode-abstract-base-class
    ///     
    ///     
    /// Released under The Code Project Open License (CPOL)
    /// </summary>
    /// <typeparam name="TEntityType"></typeparam>
    /// <typeparam name="TContextType"></typeparam>
    public abstract class RepositoryBase<TEntityType, TContextType>
        where TEntityType : class
        where TContextType : DataContext, new()
    {
        protected RepositoryBase()
        {
            //make sure TEntityType and all associated entities are suitable for use with this class. 
            Debug.Assert(CheckEntityConstraints());
        }

        /// <summary>
        /// the last exception that occurred while saving or deleting an entity
        /// </summary>
        public Exception LastException
        {
            get { return lastException; }
        }



        /// <summary>
        ///selection delegate expression, used for speeding up Load(int ID)
        /// needs to be implemented by Repository descendants as follows:
        ///
        /// protected override Expression<Func<SpecializedTEntityType, bool>> GetIDSelector(int ID)
        ///{
        ///    return (item) => item.ID == ID;
        ///}
        /// 
        /// SpecializedTEntityType represents the given entity's type
        /// and item.ID represents the entity's unique ID property
        /// 
        /// </summary>
        protected abstract Expression<Func<TEntityType, bool>> GetIDSelector(int ID);


        /// <summary>
        /// creates a new datacontext and loads the entity with the given unique ID
        /// </summary>
        /// <param name="ID">the unique entity ID</param>
        /// <returns>the entity if the ID exists, null otherwise</returns>
        public TEntityType Load(int ID)
        {
            DataContext Context = CreateContext();
            return Load(ID, Context);
        }

        /// <summary>
        /// loads the entity with the given unique ID from the given context
        /// </summary>
        /// <param name="ID">the unique entity ID</param>
        /// <param name="context">the datacontext</param>
        /// <returns>the entity if the ID exists, null otherwise</returns>
        protected TEntityType Load(int ID, DataContext context)
        {
            if (ID == 0)
            {
                return null;
            }
            return GetEntityTable(context).Single(GetIDSelector(ID));
        }

        /// <summary>
        /// Update or insert the Linq entity to the database,
        /// ignoring child entities
        /// </summary>
        /// <returns>true on success, false otherwise</returns>
        public virtual bool Save(TEntityType ToSave)
        {
            return ExecuteDatabaseOperation(ToSave, OpMode.Save, false);
        }

        /// <summary>
        /// Updates or inserts the Linq entity into the database,
        /// Child entities are saved recursively
        /// </summary>
        /// <returns>true on success, false otherwise</returns>
        public virtual bool SaveRecursively(TEntityType ToSave)
        {
            return ExecuteDatabaseOperation(ToSave, OpMode.Save, true);
        }

        /// <summary>
        ///  Deletes the given Linq entity, ignoring child entities
        /// </summary>
        /// <returns>True on success, false otherwise</returns>
        public virtual bool Delete(TEntityType ToDelete)
        {
            return ExecuteDatabaseOperation(ToDelete, OpMode.Delete, false);
        }

        /// <summary>
        ///  Deletes the given Linq entity 
        ///  All child entities are also deleted
        /// </summary>
        /// <returns>True on success, false otherwise</returns>
        public virtual bool DeleteRecursively(TEntityType ToDelete)
        {
            return ExecuteDatabaseOperation(ToDelete, OpMode.Delete, true);
        }

        /// <summary>
        /// Deletes the Linq entity with the given ID
        /// Child Entities are not deleted
        /// </summary>        
        /// <returns>true on success, false otherwise</returns>
        public bool Delete(int ID)
        {
            return Delete(Load(ID));
        }


        /// <summary>
        /// Deletes the Linq entity with the given ID
        /// All preloaded child entities are also deleted
        /// </summary>        
        /// <returns>true on success, false otherwise</returns>
        public bool DeleteRecursively(int ID)
        {
            return DeleteRecursively(Load(ID));
        }

        #region Internal and helper methods

        private Exception lastException;

        /// <summary>
        /// Save / Insert / Delete the given Linq entity depending on the given OperationMode  
        /// </summary>
        /// <returns>true on success, false otherwise</returns>
        private bool ExecuteDatabaseOperation(TEntityType theEntity, OpMode OperationMode, bool Recursively)
        {
            using (DataContext context = CreateContext())
            {
                try
                {
                    //make sure the entity is not attached to an old datacontext 
                    theEntity = EntityDetacher<TEntityType>.Detach(theEntity);
                    Table<TEntityType> entityTable = context.GetTable<TEntityType>();

                    //for some unknown reason attaching the entity before deleting it is only necessary
                    //on the topmost entity, but not on the child entities
                    if (OperationMode == OpMode.Delete)
                    {
                        entityTable.Attach(theEntity, true);
                    }

                    IterateEntity(theEntity, context, entityTable, OperationMode, Recursively);
                    context.SubmitChanges();
                }

                catch (Exception ex)
                {
                    SetException(ex, context);
                    return false;
                }
                return true;
            }
        }


        /// <summary>
        /// Save / Insert / Delete the contents of the given EntitySet
        /// </summary>
        internal void IterateEntitySet(object Set, DataContext context, OpMode OperationMode, bool Recursively)
        {
            Table<TEntityType> entityTable = GetEntityTable(context);
            foreach (TEntityType NextEntity in (EntitySet<TEntityType>)Set)
            {
                IterateEntity(NextEntity, context, entityTable, OperationMode, Recursively);
            }
        }


        /// <summary>
        /// Save / Insert / Delete the given Linq entity depending on the given OperationMode  
        /// </summary>
        /// <returns>true on success, false otherwise</returns>
        private void IterateEntity(TEntityType theEntity, DataContext context, Table<TEntityType> EntityTable, OpMode OperationMode, bool Recursively)
        {
            Debug.Assert(HasValidVersionProperty(theEntity, context));

            if (Recursively)
            {
                foreach (MetaAssociation association in context.Mapping.GetMetaType(typeof(TEntityType)).Associations)
                {
                    //only 1:n child entitites are which have been loaded are saved 
                    //check for 1:n relationship
                    if (association.IsMany && association.ThisKeyIsPrimaryKey)
                    {
                        PropertyInfo AssociationProperty = theEntity.GetType().GetProperty(association.ThisMember.Name);
                        //make sure there is at least one child entity to save
                        if (AssociationProperty.PropertyType.Name == "EntitySet`1")
                        {
                            //save the associated child entities
                            try
                            {
                                object Repository = association.OtherType.Type.GetMethod("CreateRepository").Invoke(null, null);
                                Repository.GetType().GetMethod("IterateEntitySet",
                                                             BindingFlags.NonPublic | BindingFlags.Instance).Invoke(
                                    Repository,
                                    new object[4]
                                    {
                                        AssociationProperty.GetValue(theEntity, null),
                                        context,
                                        OperationMode,
                                        Recursively
                                    }
                                    );
                            }
                            catch (System.Reflection.TargetInvocationException e)
                            {
                                throw (e.InnerException);
                            }
                        }
                    }
                }
            }

            switch (OperationMode)
            {
                case OpMode.Save:
                    if (IsNew(theEntity, context))
                    {
                        EntityTable.InsertOnSubmit(theEntity);
                    }
                    else
                    {

                        EntityTable.Attach(theEntity, true);
                    }
                    break;
                case OpMode.Delete:
                    try
                    {
                        EntityTable.DeleteOnSubmit(theEntity);
                    }
                    catch (Exception test)
                    {
                        Debug.Write("e:" + test.Message);
                    }
                    break;
            }


        }


        /// <summary>
        /// there are two constraints on the entity types that this Repositorybase is used with:
        /// - All entities must have a unique ID property
        /// - Each entity must  must implement the CreateRepository method which returns a concrete RepositoryBase descendant that matches the entity type
        /// this method checks all three constraints
        /// the checks are only carried out in when the application runs in debug mode
        /// </summary>
        private bool CheckEntityConstraints()
        {
            DataContext context = CreateContext();

            //a version attribute is not required, but advisable for speeding up 
            //save and delete operations
            if (HasVersionProperty(context))
            {
                Debug.WriteLine("Warning: \"" +
                                                  typeof(TEntityType).Name +
                                                  "\" entity type does not have a version property. You might want to add a version column to speed up Saving and Deleting of \"" +
                                                  typeof(TEntityType).Name +
                                                  "\" entities");
            }

            //make sure that TEntityType has a version attribute
            if (context.Mapping.GetTable(typeof(TEntityType)).RowType.IdentityMembers.Count != 1)
            {
                throw new NotImplementedException("\"" +
                                                  typeof(TEntityType).Name +
                                                  "\" entity type does not have a unique ID property");
            }

            /// make sure that all child linq entities of TEntityType implement a suitable "CreateRepository" Method 
            foreach (MetaAssociation association in context.Mapping.GetMetaType(typeof(TEntityType)).Associations)
            {
                if (association.OtherType.Type.GetMethod("CreateRepository") == null)
                {
                    throw new NotImplementedException("\"" + association.OtherType.Type.Name +
                                                      "\" entity type does not implement a RepositoryBase<SpecializedEntityType,DataClassesDataContext> CreateRepository() method.");
                }
            }
            return true;
        }

        private static bool HasVersionProperty(DataContext context)
        {
            return context.Mapping.GetTable(typeof(TEntityType)).RowType.VersionMember != null;
        }

        /// <summary>
        /// only new Linq entities are allowed to have a null version property. 
        /// if this is not the case then the version propery has probably gone lost while displaying the entity in a databound control
        /// </summary>
        /// <param name="Entity"></param>
        /// <returns></returns>
        private bool HasValidVersionProperty(TEntityType Entity, DataContext context)
        {
            
            if (
                HasVersionProperty(context)
                )if (
                !IsNew(Entity,context)
                && Entity.GetType().GetProperty(GetEntityVersionFieldName(context)).GetValue(Entity, null) == null)
            {
                throw new ApplicationException(Entity.GetType().Name +
                                               " has a non-zero identity property, but a null version. Check your databound controls to make sure the version attribute is retained.");
            }

            return true;
        }


        private bool IsNew(TEntityType Entity, DataContext context)
        {
            return (int) Entity.GetType().GetProperty(context.Mapping.GetTable(typeof(TEntityType)).RowType.IdentityMembers[0].Name).GetValue(Entity,null) == 0;
        }

        private Table<TEntityType> GetEntityTable(DataContext context)
        {
            return (Table<TEntityType>)context.GetTable(typeof(TEntityType));
        }

        private String GetEntityVersionFieldName(DataContext context)
        {
            return context.Mapping.GetTable(typeof(TEntityType)).RowType.VersionMember.Name;
        }

        protected virtual TContextType CreateContext()
        {
            var context = new TContextType();

            //print the resulting SQL statements in the debug output console when the application is running in debug mode
            Debug.Assert(LogDataContextOutput(context));

            return context;
        }

        bool LogDataContextOutput(DataContext context)
        {
            context.Log = new DebuggerWriter();
            return true;
        }
        //TODO: optional rethrow outside of DAL
        private void SetException(Exception Exception, DataContext context)
        {
            lastException = Exception;
            context.Log.Write(Exception.ToString());
        }

        /// <summary>
        /// Saving and Deleting entities works in a very similar fashion, so I am using using a 
        /// template function with different opmode parameters for both operations
        /// </summary>
        internal enum OpMode
        {
            Delete,
            Save
        } ;

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer Lobstersoft
Germany Germany
Adrian Grigore holds a German diploma in Computer sciences from the University of Würzburg, Germany.

He founded Lobstersoft early during his studies and has since then released several commercially successful game titles. All of these games were created using Adrian's custom build game engine.

In addition to object-oriented software design in C+ and C#, Adrian's main areas of expertise are ASP.NET, Perl and XML/XSLT.

Apart from leading Lobstersoft, Adrian also works as a freelance software development consultant.

Comments and Discussions