Archiv

Posts Tagged ‘Synchronization of DataContracts to it’s Entitties’

WCF DataContracts synchronisieren mit Entity Framework Klassen

Im Rahmen einer Projektarbeit haben wir das Ziel Businessdaten über die WCF zu transportieren. Die Daten sind im Entity Framework gespeichert. Schön wäre es wenn wir diese Entitäten direkt über den Kommunikationskanal schicken könnten. Leider ist dies nicht möglich da ein direktes Versenden der Entitäten, durch Projektrestriktionen nicht erlaubt ist, oder man ganz einfach keine Referenzen auf dem Client auf den Namespace System.Data haben möchte.

Also muss eine Lösung her die einigermassen hilfreich sein kann. Diese Lösung erledigt das Mapping eines einkommenden DatenVertrages mit der entsprechenden Entität.

Vorbereitungen
  • AddressManagement.BusinessModel
  • AddressManagement.PersistanceModel
  • AddressManagement.ServiceLayer

Für das Hosten der WCF können wir verschiedene Möglichkeiten nutzen, die aber in diesem Blogpost nicht abgedeckt werden. Es soll nur die Möglichkeit einer generischen Mapping Methode aufgezeigt werden, damit die Daten vom Entity Framework nicht direkt über den Kommunikationskanal versendet werden müssen, sondern gekapselt werden.

Wir erstellen uns ein Entity-Modell dass wie folgt aussieht: (Dieses erstellen wir im Projekt AddressManagement.Persistance)

Das Entity Data Design Model

Anschliessend erstellen wir uns unseres eigentliche Businessklassenmodell welches wir im Projekt AddressManagement.BusinessModel anlegen.

Das Businessklassen Model

Wichtig hierbei ist, dass die Beziehungen, Phones, Addresses genau gleich heissen wie im Entity Model. Auch alle anderen Eigenschaften müssen die gleiche Schreibweise haben, da sonst im Mapper keine Übereinstimmung gefunden wird.

Die Businessklassen sollen dann mit der WCF übertragen werden. Hierzu werden die Klassen mit den entsprechenden Attributen versehen.

Die Klasse Person

namespace AddressManagement.BusinessModel
{
    [DataContract]
    [KnownType(typeof(Address))]
    [KnownType(typeof(Phone))]
    public class Person : IPersistance
    {        
        #region IPersistance Members
        [DataMember]
        public Int32 Id { get; set; }
        #endregion
        [DataMember]
        public string FirstName { get; set; }
        [DataMember]
        public string LastName { get; set; }
        [DataMember]
        public IEnumerable<IPersistance> Addresses { get; set; }
        [DataMember]
        public IEnumerable<IPersistance> Phones { get; set; }
    }
}

Die Klasse Phone

namespace AddressManagement.BusinessModel
{
    [DataContract(IsReference = true)]
    [KnownType(typeof(ObjectType))]
    public class Phone : IPersistance
    {
        #region IPersistance Members
        [DataMember]
        public Int32 Id { get; set; }
        #endregion
        [DataMember]
        public string SubScriberNumber { get; set; }
        [DataMember]
        public ObjectType PhoneType { get; set; }
    }
}

Die Klasse Address

namespace AddressManagement.BusinessModel
{
    [DataContract(IsReference = true)]
    [KnownType(typeof(ObjectType))]
    public class Address : IPersistance
    {
        #region IPersistance Members
        [DataMember]
        public Int32 Id { get; set; }
        #endregion
        [DataMember]
        public string StreetName { get; set; }
        [DataMember]
        public Int16 StreetNumber { get; set; }
        [DataMember]
        public Int16 PostalCode { get; set; }
        [DataMember]
        public string CityName { get; set; }
        [DataMember]
        public ObjectType AddressType { get; set; }
    }
    [DataContract]
    public enum ObjectType
    {
        [EnumMember]
        Private,
        [EnumMember]
        Business
    }
}

Der Mapper/Synchronisierer

Damit wir nun beide Welten miteinander abgleichen können ist der Mapper zu erstellen, welche ich Schritt für Schritt erläutere. Wir erstellen uns als erstes eine Statische Klasse mit dem Namen DataContractToEntityMapper oder DataContractAndEntitySynchronizer und erstellen als erstes folgende Methode:

        /// <summary>
        /// Maps a given datacontract to the appropriate entity.
        /// Check's if the datacontract is new or must be updated
        /// and return the newly persisted contract back or it's updated
        /// values, if it was already persisted before.
        /// </summary>
        /// <typeparam name="T">Type of the datacontract</typeparam>
        /// <typeparam name="U">Type of the entity</typeparam>
        /// <param name="dataContract">The datacontract to interact with.</param>
        /// <returns>A new or updated datacontract from the database.</returns>
        public static IPersistance SynchronizeDataContractAndEntity<T, U>(T dataContract)
            where T : IPersistance
            where U : EntityObject
        {
            IPersistance persistance = Activator.CreateInstance<T>();
            // 1. a) Determine if the datacontract is already persisted.
            if (dataContract.Id == 0)
            {
                // 1. b) Map the newly created entity to the datacontract.                
                PersistDataContract<T, U>(dataContract);
            }
            else
            // 2.) Map an existing entity to it's datacontract pendant
            {
                U persistedEntity = Activator.CreateInstance<U>();
                using (AddressRepository<U> repository = new AddressRepository<U>(new AddressManagementPersistanceModelContainer()))
                {
                    persistedEntity = repository.GetByKey<U>(dataContract.Id);
                    persistance = UpdateProperties<T, U>(dataContract, persistedEntity);
                }
            }
            return persistance;
        }

Diese Methode führ folgende Aktionen durch:

  1. Existiert der eingetroffene DataContract schon in der Datenbank?
  2. Ist die Id 0, dann muss eine neue Entität angelegt werden
  3. Ist die Id > 0 ist der Contract bereits in der Datenbank vorhanden und muss mit den vorliegenden Werten aktualisiert werden.
  4. Zum Schluss, nachdem die Entity und der DataContract miteinander synchronisiert worden sind, wird ein aktualisierter DataContract zurück gegeben.

Als nächster Schritt erstellen wir uns eine Methode die für den Abgleich der Property-Werte zuständig ist. Diese unterscheidet ob es sich bei der Eigenschaft um eine Collection oder um eine einfache Eigenschaft handelt und führt dementsprechend die Aktualisierung aus:

        /// <summary>
        /// Updates the properties from a datacontract.
        /// </summary>
        /// <typeparam name="T">The type of datacontract</typeparam>
        /// <typeparam name="U">The type of the entity</typeparam>
        /// <param name="dataContract">The datacontract to update the values.</param>
        /// <param name="entity">The entity that is persisted and owns the actual values.</param>
        /// <returns>Updated datacontract.</returns>
        private static IPersistance UpdateProperties<T, U>(T dataContract, U entity)
            where T : IPersistance
            where U : EntityObject
        {
            IPersistance persistance = (IPersistance)CreateInstance<T, U>(dataContract, entity);
            entity.GetType().GetProperties().ToList().ForEach(entityPropertyInfo =>
            {
                string propertyName = entityPropertyInfo.Name;
                PropertyInfo dataContractPropertyInfo = persistance.GetType().GetProperty(propertyName);
 
                if (entityPropertyInfo != null && !entityPropertyInfo.PropertyType.Name.Contains(typeof(EntityCollection<U>).Name))
                {
                    var entityPropertyValue = entityPropertyInfo.GetValue(entity, null);
                    if (entityPropertyValue != null && dataContractPropertyInfo != null && !dataContractPropertyInfo.PropertyType.IsEnum)
                    {                        
                        dataContractPropertyInfo.SetValue(persistance, entityPropertyValue, BindingFlags.Default, null, null, null);
                    }
                }
                else if (entityPropertyInfo != null && dataContract.GetType().GetProperty(entityPropertyInfo.Name) != null)
                {
                    var ienumerableValues = (IEnumerable)HandleRelatedEnds<T>(dataContract, (IEnumerable)entityPropertyInfo.GetValue(entity, null));
                    persistance.GetType().GetProperty(entityPropertyInfo.Name).SetValue(persistance, ienumerableValues, BindingFlags.Default, null, null, null);
                }
            });
            return persistance;
        }

Der erste Teil aktualisiert alle flachen Eigenschaften des DatenContracts. Der zweite Teil kümmert sich um die Collections.

        /// <summary>
        /// Handles the collections an creates for the datacontract
        /// appropriate items to add them to the list.
        /// </summary>
        /// <typeparam name="T">Type of datacontract</typeparam>
        /// <param name="dataContract">The specific contract where the list belongs to.</param>
        /// <param name="entityCollectionToDuplicate">The entitycollection to map to the datacontract.</param>
        /// <returns>A List of IPersistance objects (BusinessModel Objects).</returns>
        private static List<IPersistance> HandleRelatedEnds<T>(T dataContract, IEnumerable entityCollectionToDuplicate) where T : IPersistance
        {
            List<IPersistance> persistances = new List<IPersistance>();
            IEnumerator it = entityCollectionToDuplicate.GetEnumerator();
 
            while (it.MoveNext())
            {
                EntityObject entity = ((EntityObject)it.Current);
                IPersistance ipersistance = CreateInstance<T, EntityObject>(dataContract, entity);
                ipersistance = UpdateProperties<T, EntityObject>(dataContract, entity);
                persistances.Add(ipersistance);
            }
 
            return persistances;
        }

Damit die Items erstellt werden können braucht es eine weitere Hilfsmethode die uns entsprechende Instanzen zurück gibt.

        /// <summary>
        /// This method creates a specific instance from the Businessmodell Assembly
        /// given by the entity type.
        /// </summary>
        /// <typeparam name="T">The IPersistanceObject to create</typeparam>
        /// <typeparam name="U">The entity that contains the values</typeparam>
        /// <param name="dataContract">The datacontract to add /remove or update</param>
        /// <param name="entity">The entity that was created /updated depending on the passed datacontract.</param>
        /// <returns></returns>
        private static IPersistance CreateInstance<T, U>(T dataContract, U entity) where T : IPersistance
        {
            IPersistance persistance;
            string typeOfEntity = entity.GetType().Name;
            Assembly assembly = Assembly.GetAssembly(typeof(IPersistance));
            Type type = assembly.GetTypes().FirstOrDefault(assemblyType => assemblyType.Name.ToLower().Equals(typeOfEntity.ToLower()));
            persistance = (IPersistance)assembly.CreateInstance(type.FullName);
 
            return persistance;
        }

[/csharp]
<p>Der Funktionsumfang der oben beschriebenen Methode ist einfach. Nimm den Entity-Namen und suche in der BusinessModel Library nach dem entsprechenden Typ und instanziiere einen solchen und gib diesen dann zurück, damit er in der aufrufenden Methode in die Liste eingefügt werden kann.
Eine weitere Methode brauchen wir, damit wir einen DataContract, der über keinen Wert in der Id verfügt, persistieren können. (Das Repository ist nicht Teil dieses).
</p>

        /// <summary>
        /// A newly passed datacontract with it's properties
        /// will be persisted in the entity framework.
        /// </summary>
        /// <typeparam name="T">DataContractType</typeparam>
        /// <typeparam name="U">EntityType (must macht with the datacontract type)</typeparam>
        /// <param name="dataContract">The datacontract to persist.</param>
        private static void PersistDataContract<T, U>(T dataContract) where T : IPersistance where U : EntityObject
        {
            U entityToPersist = Activator.CreateInstance<U>();
            entityToPersist.GetType().GetProperties().ToList().ForEach(dataContractPropertyInfo =>
            {
                string propertyName = dataContractPropertyInfo.Name;
                PropertyInfo entityPropertyInfo = dataContract.GetType().GetProperty(propertyName);
 
                if (entityPropertyInfo != null && entityPropertyInfo.PropertyType != (typeof(RelatedEnd)))
                {
                    var entityPropertyValue = entityPropertyInfo.GetValue(dataContract, null);
                    if (entityPropertyValue != null)
                    {
                        dataContractPropertyInfo.SetValue(entityToPersist, entityPropertyValue, BindingFlags.Default, null, null, null);
                    }
                }
            });
 
            using (AddressRepository<U> repository = new AddressRepository<U>(new AddressManagementPersistanceModelContainer()))
            {
                repository.Add(entityToPersist);
                repository.SaveChanges();
            }
        }

Mit dieser letzten Methode haben wir den Mapper erstellt. Nun ist es uns möglich von einem Sender ein Objekt der Businessklassen zu erstellen und müssen uns keine Sorgen machen, ob dies bereits persistiert worden ist oder nicht. Wir erhalten als dann die aktualisierten oder neuen Werte von der Datenbank als Resultat zurück und können somit auf dem Client weiter arbeiten.

Fazit

Der Mapper hat nicht den Anspruch 100% aller Fälle abzudecken, er versucht zu zeigen wie man eine grösstmögliche Modularisierung anstreben kann, ohne dass auf dem Client Server DLL’s refernziert werden müssen. Und natürlich ein nicht zu unterschätzender Punkt ist der Aufwand einmal für die WCF Seite und einmal für die Persistance-Seite ein Modell zu führen.

Sollte Ihnen der Artikel gefallen haben, würde ich mich über einen kick freuen. Gerne beantworte ich auch Fragen und Kritik.

kick it on dotnet-kicks.de

Advertisements