Archiv

Archive for the ‘WCF’ Category

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

WCF Service über JQuery ansprechen leicht gemacht

Zur Zeit bin ich an einer Weiterbildung für die Verwendung von .NET im Grossfirmenumfeld. Ein Thema darin ist ist Windows Communication Foundation (kurz WCF genannt).

Da ich aus der Ecke der Webprogrammierer komme interessiert es mit primär, wie ich einen WCF-Service von einer Webseite her ansprechen kann.

Was brauchen wir denn für einen Service der auf der Basis der WCF für einen Webseitenzugriff konfiguriert ist?

Vorbereitungen

  1. Erstellen eines neues ASP.NET Projektes über den Menüpunkt „Neues Projekt erstellen…“
  2. Mit NuGet zuerst die JQueryUI und dann die JQuery
    Libraries hinzufügen
  3. Erstellen Sie einen Ordner Services in der Solution
  4. In den zuvor erstellten Service fügen Sie ein neues Item hinzu (Vorlage für einen
    WCF-Service) und benennen Sie diesen mit „ImageService“
  5. Visual Studio erstellt uns eine .svc und ein entsprechendes Interface ImageService.cs.
    Hier stehen noch nicht allzu viele Sachen drin sodass wir die DoWork Methode im
    Interface ein wenig anpassen. Nach dem Hinzufügen des Service sollte die Solution
    mit den aktuellen JQuery Referenzen ausgestattet sein und folgende .NET Dll’s als
    Referenz aufweisen:

    • System.Service.Model
    • System.Runtime.Serializatin

WCF Konfiguration

Soweit so gut. Wir sind bereit den WCF Service zu implementieren. Hierzu wird in die Solution noch ein Verzeichnis Images hinzugefügt, dass einfach mit den Beispiel Bildern von Windows befüllt wird. Zuerst fangen wir mit der Konfiguration der Web.Config für unseren Dienst an.

Konfiguration der Web.Config mit dem „WCF Service Configurator“ (Finden Sie unter Tools bei Visual Studio 2010)

  1. Erstellen eines neuen Services
  2. Auswahl der DLL von welcher her der Service generiert werden soll. Sollte im bin
    Verzeichnis keine DLL vorliegen so kann man dies mit dem „Erstellen der Solution“
    beheben
  3. Den Vertrag (Contract) können wir stehen lassen und weiter klicken.
  4. Da wir einen ImageService für’s Internet bereitstellen wollen, lassen wir die Konfiguration
    auf http sein.
  5. Auch weitere Optionen belassen wir auf dem Standard. Wir wollen einen einfachen
    ImageService ohne Duplex Funktion und lassen den Vorschlag des Assistenten so wie
    er ist.
  6. Zum Schluss erstellen wir die Endpunkt-Adresse über welche der Service erreicht
    werden soll.
  7. Wir haben einen neuen Service erstellt, wie uns der Assistent nun mitteilt.

Anpassen des WCF Service für unsere Bedürfnisse

Nun haben wir den Service deklarativ in der Web.Config erstellt. Nun müssen wir noch Anpassungen vornehmen, damit der Service auch mit einer WebHttpBinding angesprochen werden kann (das Programmiermodell http von WCF).

Anpassen des erstellten Service für die Verwendung mit ASP.NET (WCF Service Configurator).

  1. Selektieren Sie den zuvor erstellten Endpunkt
  2. Das Binding muss mit der DropDownListe auf webHttpBinding gewechselt werden.
  3. Nun müssen wir dem Binding noch eine Konfiguration zuordnen. Dies können wir tun
    indem wir den Punkt „Bindings“ auswählen und ein neues hinzufügen. ACHTUNG:
    Man muss das WebHttpBinding auswählen, damit die erstellte Konfiguration darauf
    wirkt.
  4. Wir geben der neu erstellen Konfiguration einen Namen und belassen die Standardwerte
    so wie sie sind (Die Ausnahmen werden nachstehend aufgelistet).

    Konfigurationselement Wert Grund
    ByPassProxyOnLocal True Lokale Adressen werden nicht über einenProxy-Server aufgelöst.
    CrossDomainScriptAccessEnabled True Zugriff von überall her möglich per Script (Internet Service).
  5. Nun wechseln wir wieder zu den Endpunkten und wählen unseren erstellten Endpunkt
    aus. Danach können wir in der Bindungskonfiguration die neu erstellte Bindung auswählen.

Den WCF Service zum Leben erwecken

Wir erstellen uns eine Global.asax die für das Starten und Beenden der WebApplikationen die entsprechenden Methoden bereitstellt. Interessant sind die Methoden Application_Start und die Application_End.

  1. Wir fügen unserem Webprojekt eine Global.asax mit dem Befehl Add new Item… hinzu.
  2. Damit wir webHttp verwenden können müssten wir System.ServiceModel.Web referenzieren
    damit wir ein Objekt vom Typ WebserviceHost erstellen können.
  3. Die Global.asax sieht dann wie folgt aus:

     using System; using System.ServiceModel.Web; using
                    Sample.WebHttpImageService.Services; namespace Sample.WebHttpImageService { public
                    class Global : System.Web.HttpApplication { ///
                    <summary>
            /// Host is declared here to not been removed by
            /// the carbage collector.
            /// </summary>
                    private static WebServiceHost host = new WebServiceHost(typeof(ImageService), new
                    Uri("http://localhost:8099")); ///
                    <summary>
            /// Occurs when application is staring up.
            /// </summary>
                    ///
                    <param name="sender"></param>
                    ///
                    <param name="e"></param>
                    protected void Application_Start(object sender, EventArgs e) { try { host.Open();
                    } catch (Exception exception) { host.Abort(); } } ///
                    <summary>
            /// Occurs at application shutdown.
            /// </summary>
                    ///
                    <param name="sender"></param>
                    ///
                    <param name="e"></param>
                    protected void Application_End(object sender, EventArgs e) { host.Close(); } } }
                    
  4. Startet man nun das Projekt kann unter Umständen die Fehlermedung kommen, dass man
    eine nicht registrierte URL versucht aufzurufen. Abhilfe schafft hier das Visual
    Studio als Administrator zu starten (generell mit WCF zu empfehlen).

  5. Nun wollen wir prüfen ob der Service mit der Methode GetImages angesprochen werden
    kann. Die machen wir indem wir http://localhost:8099/ImageService/GetImages in den
    Browser eingeben. Wir eine Fehlermeldung, die besagt dass die Methode nicht ausgeführt
    werden kann.

    Ist ja eigentlich logisch, denn unsere momentane Service Methode erwartet einen
    Parameter der über Web „GET“ übermittelt wird. Dies wurde noch nicht implementiert.

  6. Ist das Attribut im Interface angegeben können wir zum Beispiel mit dem Auftruf
    http://localhost:8099/ImageService/GetAllImages?path=Test einen Methodenaufruf machen
    und erhalten das folgende Beispiel Resultat.


Anpassen der WCF Methode GetImages

Nun wollen wir doch, dass der Service über einen AJAX Aufruf per JQuery angesprochen werden kann. Hierzu gehen wir wie folgt vor. Da die Service-Methode noch nicht so tut wie wir das gerne wollen, dekorieren wir die Methode im Interface mit dem Attribute WebGet. Das Interface sieht nun wie folgt aus:

[ServiceContract]
public Interface IImageService{
[OperationContract]
[WebGet]
public List<string> GetImages(string path);
}

Die Implementierungsklasse sieht dann so aus:

public class ImageService : IImageService{
public List<string> GetImages(string path){
List<string> result = new List<string>(){ "Hello WCF"};
return result;
}
}

Clientseitige Implementierung

Nun kommen wir auf die Clientseite. Wir fügen, falls es nicht schon getan wurde, ein Default.aspx WebForms zur Solution hinzu.

Hierzu erstellen wir uns unter dem Verzeichnis (Scripts) ein Unterverzeichnis mit dem Namen „Custom“ und fügen uns dort eine ImageService.js Datei hinzu, die folgenden Inhalt aufweist. (Vergessen Sie nicht die JQuery Scriptreferenz im Header der Default.aspx hinzuzufügen).

Unser erstelltes Clientscript sieht dann so aus:

$(document).ready(function () {
    var imagesUri = "http://localhost:7777/Images/";

    jQuery.support.cors = true;

    $.ajax({

        type: "GET",
        url: "http://localhost:8099/ImageService/GetImages?path=" + imagesUri,
        dataType: "text",
        contentType: "application/text; charset=utf-8",
        success: function (message) {
            //$('#Result').append(message);
            var xmlDocument = jQuery.parseXML(message);
            var images = xmlDocument.getElementsByTagName("string");

            for (var index = 0; index <= images.length - 1; index++) {

                $('#Result').append("<img src=\'" + images[index].textContent + "\' />");

            }
        },
        error: function (message) {

        }
    });

});

Die Default.aspx braucht dann nur noch eine Script-Referenz auf diese Datei.

<head runat="server">
    <title>Zugreifen auf WCF mittels JQuery</title>  
    <script type="text/javascript" language="javascript" src="Scripts/jquery-1.7.1.js"></script>  
    <script type="text/javascript" language="javascript" src="Scripts/Custom/ImageService.js" ></script>
</head>

Der httpQuery muss mit dem Parameter der im Inteface für die Servicedefinition festgelegt worden ist übereinstimmen. Nun ist es nicht wirklich ansprechend ein Hallo reinzugeben und dann als Resultat Hallo WCF, mit den geschweiften Klammern zu erhalten.

Wichtig hierfür ist das JQuery für CrossDomain zugriff konfiguriert wird. Dies geschieht wie folgt:

	jQuery.support.cors = true;

Nun da WCF und ASP.NET nicht im gleichen Kontext beheimatet sind kann die Grundlage beider genommen werden. Beide bauen auf System.Web.Hosting auf. Folgenes Beispiel lässt alle Bilder die vom Pfad übergeben worden sind mit dem DirectoryInfo suchen und finden.

using System;
using System.Collections.Generic;
using System.IO;
using System.Web.Hosting;
using System.Linq;

namespace Sample.WebHttpImageService.Services
{
    /// <summary>
    /// Get's the images from the path given by parameter.
    /// </summary>
    /// <param name="path">ImagePath to look for images.</param>
    public class ImageService : IImageService
    {
        public string[] GetImages(string path)
        {
            List<string> result = new List<string>();
            
            Uri uri = new Uri(path, UriKind.Absolute);
            
            FileInfo[] images = new DirectoryInfo(HostingEnvironment.MapPath(uri.LocalPath)).GetFiles();

            images.ToList().ForEach(image =>
                                        {
                                            string imageUrl = string.Format(@"{0}{1}", path, image.Name);
                                            result.Add(imageUrl);
                                        });

            return result.ToArray();
        }   

Unser Image Service muss so dann nicht im ApsNetCompatibilityMode betrieben werden und wir können trotzdem auf Files auf dem WebServer zugreifen. Nähere Details können hier gefunden werden WCF Service and ASP.NET

Zu guter Letzt Verarbeiten wir Client-seitig das XML das wir vom Server zurück erhalten und zeigen die Bilder an.

Das Bilder anzeigen haben wir ja in die ImageService.js ausgelagert.

Fazit

Mit eigentlich ganz wenig Aufwand ist es möglich einen Service zur Verfügung zu stellen der von allen Interessierten konsumiert werden kann. Trickreich an den ganzen Recherchen war auf jeden Fall das Zusammenspiel zwischen ASP.NET und der WCF die ja nicht im gleichen Kontext laufen, ohne dass man das AspNetCompatibilityReruirement Attribut braucht, oder den WCF-Service im Kompatibilitätsmodus laufen lässt. Weiter werde ich versuchen in den nächsten Wochen ein wenig mehr um die Daten die ausgetauscht werden, zu beleuchten.

Wenn Ihnen dieser Artikel gefallen hat, würde ich mich über einen Kick freuen und bin auch empfänglich für Rückmeldungen und Kritik.

kick it on dotnet-kicks.de