Automatische Applikationsverteilung mit Powershell

Herausforderung

Damit wir zukünftig Script-basiert unsere Applikation und die damit verbundenen Produkte automatisch verteilen wollen, habe ich, auftrund der Flexibilität der Powershell ein entsprechendes Script erstellt.

Dieses Script verteilt unter anderem:

  • Dienste
  • Anwendungen
  • Webseiten

Ziele

Folgende Ziele sollen dafür erreicht werden:

  • Die Dienste werden konfiguriert und mit der entsprechenden Datenbankverbindung gestartet (Entwicklungs-, Integrations- oder Produktionsumgebung).
  • Gleiches Ziel gilt für die Anwendung
  • Gleische Ziel gilt auch für die Webseite

Durchführung

Die Anwendungen und die Webseiten übrprüfen beim Start in welchem Ordner sie sich befinden und wählen automatisch die korrekte Konfiguration aus. Dies gilt nicht für die Dienste, also müssen die .config Dateien nach dem Verteilen angepasst werden, sodass der Dienst auf die richtige Datenbank verbinden kann.

Hierzu wird das folgende Powershell Script erstellt, welches Schritt für Schritt erläutert wird.

Vorbereitung

Das SetUp des Scriptes sieht wie folgt aus:

param([Parameter(Mandatory=$true)][string]$environment, [Parameter(Mandatory=$true)][string]$buildPath)

$server = [string]::Empty;
$deployPath = [string]::Empty;
$dbServer = [string]::Empty;
$global:session = $null;

if($environment.Equals('Entwicklung')){
    $server = "Entwicklungsserver-01";  
    $deployPath = "\\EntwicklungsZielServer-01\D$\Produkt";      
    $dbServer = "EntwicklungsDbserver-01";
    $session = New-PSSession -ComputerName $server
}elseif($environment.Equals('Integration')){
    $server = "Integrationsserver-01";  
    $deployPath = "\\Integrationsserver-01\D$\Produkt";      
    $dbServer = "Integrationsserver-01";
    $session = New-PSSession -ComputerName $server;
}elseif($environment.Equals('Production')){
    $server = "Produktionsserver-01";  
    $deployPath = "\\Produktionsserver-01\D$\Produkt";      
    $dbServer = "Produktionsserver-01";
    $session = New-PSSession -ComputerName $server;
}

Das SetUp überprüft an der Stelle, welcher Parameter am Anfang mit gegeben worden ist und stellt die Werte dementsprechend zusammen.

Verteilung der Benutzerapplikation

In unserem Fall ist diese Applikation eine Desktop-Applikation, die vom entsprechenden Share aus gestartet wird. Aufgrund in welchem Ordner diese liegt, wird dann die richtige Verbindungszeichenfolge genommen. Die Funktion sieht so aus:

Function DeployProduct(){   
        
    Write-Host('Deploying ' + $buildPath + ' in ' + $environment);

    Write-Host('Deploy Produkt 01');

    if($environment.Equals('Entwicklung'))
    {
        copy-Item -Path (Get-Item -Path (Join-Path -Path $buildPath -ChildPath "Produkt-01")) -Recurse -Force -Destination "\\Share-01\DEVELOPMENT";
    }
    elseif($environment.Equals('Integration'))
    {
        copy-Item -Path (Get-Item -Path (Join-Path -Path $buildPath -ChildPath "Produkt-01")) -Recurse -Force -Destination "\\Share-01\INTEGRATION";
        Write-Host('CleanUp xml and pdb files');
        Get-ChildItem -Path "\\Share-01\Produkt-01" -Recurse -Force -Include '*.pdb', '*.xml', '*sccm*exe', '*.nupkg', '*.nuspec' | Remove-Item -Force;
    }
    elseif($environment.Equals('Production'))
    {
        copy-Item -Path (Get-Item -Path (Join-Path -Path $buildPath -ChildPath "Produkt-01")) -Recurse -Force -Destination "\\Share-01\PRODUCTION";
        Write-Host('CleanUp xml and pdb files');
        Get-Item -Path "\\Share-01\PRODUCTION\Produkt-01" -Recurse;
    }
}

Somit hätten wir die Applikation unseres Produktes verteilt.

Dienstverteilung

Bei der Dienstverteilung gibt es einen Fallstrickt, den ich hier vorenthalten möchte und zwar MUSS man, wenn man die .config Datei anpassen will, diese wie folgt speichern.

(Get-Content -Path $_.FullName).Replace('SAIFC1-AISQLD-03', $dbServer) | Out-File -FilePath $_.FullName -Force -Encoding utf8;

Ganz wichtig hierbei ist, dass man die Datei im UTF-8 Format speichert. Ansonsten kann es sein, dass man den Fehler ‚Side-by-Side‘ erhält.

Nun die ganze Verteilung der Dienste, wie ich sie vorgenommen habe:

Stoppen der Dienste

Function StopServices(){
    Get-Service -ComputerName $server -Include "*%DienstName%*" | Stop-Service -Force;    
}

Kopieren des Dienstes

Function ServiceDeployment(){      
    if($environment.Equals('Entwicklung'))
    {        
        copy-Item -Path (Get-Item -Path (Join-Path -Path $buildPath -ChildPath "DailyService")) -Recurse -Force -Destination $deployPath;
    }
    elseif($environment.Equals('Integration'))
    {
        copy-Item -Path (Get-Item -Path (Join-Path -Path $buildPath -ChildPath "DailyService")) -Recurse -Force -Destination $deployPath;
        Write-Host('CleanUp xml and pdb files');
        Get-ChildItem -Path $deployPath -Recurse -Force -Include '*.pdb', '*.xml', '*sccm*exe', '*.nupkg', '*.nuspec' | Remove-Item -Force;
    }
    elseif($environment.Equals('Production'))
    {
        copy-Item -Path (Get-Item -Path (Join-Path -Path $buildPath -ChildPath "DailyService")) -Recurse -Force -Destination $deployPath;
        Write-Host('CleanUp xml and pdb files');
        Get-Item -Path $deployPath -Recurse -Force -Include '*.pdb', '*.xml', '*sccm*exe', '*.nupkg', '*.nuspec' | Remove-Item -Force;
    }    
}

Änderung der Verbindungszeichenfolge

Function ChangeConnectionStrings(){
    
    Get-ChildItem -Path $deployPath -Filter "*.config*" -Recurse | ForEach-Object{                     
        (Get-Content -Path $_.FullName).Replace('EntwicklungsDbServer-01', $dbServer) | Out-File -FilePath $_.FullName -Force -Encoding utf8;        
    }
}

Starten des Dienstes

Function StartServices(){
    Get-Service -ComputerName $server -Include "*%DienstName%*" | Start-Service;    
}

Fast haben wir es geschafft.

Verteilung der Webseite

Da die Webseite wie auch die Applikation weiss mit welchem Datenbankserver sie sich verbinden muss, wenn sie in einem bestimmten Ordner liegt, muss man hier nur folgende Aktivitäten durchführen:

  • Stoppen der jeweiligen Webseite
  • Kopieren des Inhaltes der Webseite
  • Starten der Webseite
Function DeployWebSite(){
$session = New-PSSession -ComputerName InternetServer-01;
    if($environment.Equals('Entwicklung'))
{
    Invoke-Command -Session $session -ScriptBlock { Stop-Website -Name Web-Entwicklung };
    copy-Item -Path (Get-Item -Path (Join-Path -Path $buildPath -ChildPath "Web-Entwicklung\_PublishedWebsites\Web-Entwicklung")) -Recurse -Force -Destination "\\InternetServer-01\D$\WebSites\Development";
    Invoke-Command -Session $session -ScriptBlock { Start-Website -Name Web-Entwicklung };
}
elseif($environment.Equals('Integration'))
{
    Invoke-Command -Session $session -ScriptBlock { Stop-Website -Name Web-Integration };
    copy-Item -Path (Get-Item -Path (Join-Path -Path $buildPath -ChildPath "Web-Integration\_PublishedWebsites\Web-Integration")) -Recurse -Force -Destination "\\InternetServer-01\D$\WebSites\Integration";
    Write-Host('CleanUp xml and pdb files');
    Get-ChildItem -Path "\\InternetServer-01\D$\WebSites\Integration" -Recurse -Force -Include '*.pdb', '*.xml' | Remove-Item -Force;
    Invoke-Command -Session $session -ScriptBlock { Start-Website -Name Web-Integration };
}
elseif($environment.Equals('Production'))
{    
    Invoke-Command -Session $session -ScriptBlock { Stop-Website -Name Web-Produktion };
    copy-Item -Path (Get-Item -Path (Join-Path -Path $buildPath -ChildPath "Web-Produktion\_PublishedWebsites\Web-Produktion")) -Recurse -Force -Destination "\\InternetServer-01\D$\WebSites\Production";
    Write-Host('CleanUp xml and pdb files');
    Get-ChildItem -Path "\\InternetServer-01\D$\WebSites\Integration" -Recurse -Force -Include '*.pdb', '*.xml' | Remove-Item -Force;
    Get-Item -Path "\\InternetServer-01\D$\WebSites\Production" -Recurse;
    Invoke-Command -Session $session -ScriptBlock { Start-Website -Name Web-Produktion };
}
}

Aufgrung dessen, dass der Windows Server 2012R2 bereits die Module für die Administration des IIS mittels Powershell instaliert hat, wird hier eine Remote-Session eröffnet, damit dann das Kommando remote ausgelöst werden kann. (ACHTUNG: Der Benutzer muss hierfür und auch für alle anderen Aktionen die entsprechende Berechtigung auf dem Server haben).

Fazit

Mit Powershell lassen sich mehr oder weniger einfache Verteilmechanismen etablieren, die einem zum Anderen eine gewisse Flexibilität geben und zum Anderen sehr gut an die eigenen Bedürfnisse angepasst werden können.

ASP.NET MVC 5 Paging selber implementiert

Bei meinem aktuellen Projekt, wollte ich das Paging in einer MVC Applikation selber, ohne Mithilfe der NuGet PagedList for MVC, realisieren.

Zuerst der Reihe nach

  1. Wie wird die erste Seite aufgerufen?
  2. Wie werden dann die folgenden Seiten aufgerufen?

Wie wird die erste Seite aufgerufen?

Zuerst schauen wir uns den HTML Markup des Linkes an, bei welchem wir zur entsprechenden Seite navigieren.


Wie ihr seht ist dass ein ganz normaler Querystring über http

Die Seite wird dann über folgende ActionMethode aufgerufen, wie auch die Seiten die dann folgen. Zum Beispiel Seite 2 etc.

Wie werden dann die folgenden Seiten aufgerufen?

            int toalPageCount = (context.Set<Skater>().Where(skater => skater.IsActive && skater.IsSEV).OrderBy(skater => skater.Lastname).Count() / 10);
            int pageCounter = 0;

            Dictionary<int, List<SkaterDTO>> pages = new Dictionary<int, List<SkaterDTO>>();
            List<SkaterDTO> DTO = new List<SkaterDTO>();                        

            if (selectedPage == 1)
            {
                context.Set<Skater>().OrderBy(skater => skater.Lastname).Where(skater => skater.IsActive && skater.IsSEV).Take(10).ToList().ForEach(skater =>
                {
                    var vm = new SkaterDTO(skater);
                    DTO.Add(vm);
                });
                pages.Add(toalPageCount, DTO.ToList());
                pageCounter++;                    
            }
            else if (selectedPage > 1)
            {
                if (selectedPage == toalPageCount)
                {
                    context.Set<Skater>().OrderByDescending(skater => skater.Lastname).Where(skater => skater.IsActive && skater.IsSEV).Take(10).ToList().ForEach(skater =>
                    {
                        var vm = new SkaterDTO(skater);
                        DTO.Add(vm);
                    });
                }
                else
                {
                    context.Set<Skater>().OrderBy(skater => skater.Lastname).Where(skater => skater.IsActive && skater.IsSEV).Skip(selectedPage * 10).Take(10).ToList().ForEach(skater =>
                    {
                        var vm = new SkaterDTO(skater);
                        DTO.Add(vm);
                    });
                }
                pages.Add(toalPageCount, DTO.ToList());
                pageCounter++;                                        
            }
                       

            return PartialView("Skaters", pages);

Die HTML Seite für die Darstellung des Resultates sieht dann wie folgt aus:

@model System.Collections.Generic.Dictionary<int, System.Collections.Generic.List<EislaufSektionWebUI.DTO.SkaterDTO>>

@{
    ViewBag.Title = "Läufer(innen)";
    Layout = "~/Views/Shared/_LayoutPage.cshtml";
}

@Html.ActionLink("Zurück", "Welcome", "Home")

<table class="table table-responsive table-bordered table-hover">

        <thead>
            <tr>
                <th>
                    Vorname
                </th>
                <th>
                    Name
                </th>
                <th>
                    Bestandene Tests
                </th>
                <th>
                    SEV
                </th>
                <th>
                    Geburtsdatum
                </th>
            </tr>
        </thead>
        <tbody>
            @{               
                var currentPage = int.Parse(HttpContext.Current.Request.QueryString["selectedPage"]);
                foreach (var skater in Model[currentPage-1].ToList())
                {
                    <tr>
                        <td>
                            @skater.Firstname

                        </td>
                        <td>
                            @skater.Lastname
                        </td>
                        <td>
                            @skater.CertificationLevel
                        </td>
                        <td>
                            @skater.IsSEV
                        </td>
                        <td>
                            @skater.DateOfBirth
                        </td>
                    </tr>
                }
            }
        </tbody>
    </table>

<nav>
    <ul class="pagination">
        <li>
            <a href="#" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
        @{
            foreach (var key in Model.Keys)
            {
                var number = key + 1;
                <li>
                    <a href="/Person/GetSkaters?selectedPage=@number">@number</a>
                </li>
            }
        }
        <li>
            <a href="#" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
    </ul>
</nav>

Die Twitter Bootstrap „pagination“ Links werden dann wie folgt generiert:

<nav>
    <ul class="pagination">
        <li>
            <a href="#" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
        @{
            foreach (var key in Model.Keys)
            {
                var number = key + 1;
                <li>
                    <a href="/Person/GetSkaters?selectedPage=@number">@number</a>
                </li>
            }
        }
        <li>
            <a href="#" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
    </ul>
</nav>

Ich hoffe euch hat der Beitrag gefallen und freue mich über Rückmeldungen oder konstruktive Kritik.

Entity Framework 5.0 und 1 zu n Beziehungen

Normalerweise wird ja bei der Architektur darauf geachtet, dass man so generell wie möglich ist. Im konkreten Fall mit Listen, werden in sehr vielen Fällen IEnumerable als Typ genommen, damit man alle Klassen die diese Schnittstelle implementieren, auf diese Eigenschaft anwenden kann. Nun kann es aber sein, dass man in Zusammenhang mit Code-First das eine oder andere Phänomen beobachtet, nämlich die Schlüsselgenerierung in der Datenbank. Als Beispiel für diesen Blogpost soll eine kleine Adressverwaltung dienen bei denen eine Person mehrere Adressen haben kann. Einen interessanten Beitrag Finden Sie hier auf Stackoverflow.

Ausgangslage

Die Ausgangslage bildet ein relativ einfaches Modell, in welchem wir folgende Klassen haben:

  • Person
  • Address

Wobei gemäss Modell eine Person mehr als eine Adresse haben kann. Nun auf der Datenbank benötigt man hier auf der Seite der Adressen-Tabelle einen Schlüssel, der auf die Person zeigt, die mit dieser Adresse assoziiert ist.

AssociationAttribute

Wenn die Klasse Person wie folgt aussieht:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;

namespace AddressManager.Models
{
    ///
    /// Model class for a person.
    ///
    public class Person : PersistanceBase
    {
        ///
        /// Get's or set's the value for Firstname.
        ///
        [Required]
        public string Firstname { get; set; }
        ///
        /// Get's or set's the value for Lastname.
        ///
        [Required]
        public string Lastname { get; set; }
        ///
        /// Get's or set's the value for DateOfBirth.
        ///
        [DataType("datetime2")]
        public DateTime DateOfBirth { get; set; }
        ///
        /// Get's or set's the value for the assigned
        /// Addresses.
        ///
        public virtual IEnumerable<Address>Addresses { get; set; }
       ///
       /// Initializes a new instance of Person.
       ///
       public Person()
       {

       }
   }
}

dann fällt auf das die Adressen durch eine IEnumerable in Beziehung gebracht werden. Will man nun auf der Adresse eine Beziehung zur Person aufbauen, so muss in der Klasse Adresse eine weitere Eigenschaft hinzugefügt werden. Die Klasse Adresse sieht dann so aus:

using System.ComponentModel.DataAnnotations;

namespace AddressManager.Models
{
    ///
    /// Model class for an address
    /// associated to a person.
    ///
    public class Address : PersistanceBase
    {
        [Association("Address_Habitant", "Id", "Id", IsForeignKey = true)]
        public Person Habitant { get; set; }
        ///
        /// Get's or set's the value for Streetname.
        ///
        [Required]
        public string Streetname { get; set; }
        ///
        /// Get's or set's the value for Streetnumber.
        ///
        [Required]
        public int Streetnumber { get; set; }
        ///
        /// Get's or set's the value for Streetextension.
        ///
        public string Streetextension { get; set; }
        ///
        /// Get's or set's the value for Postalcode.
        ///
        [Required]
        public int Postalcode { get; set; }
        ///
        /// Get's or set's the value for City.
        ///
        [Required]
        public string City { get; set; }
        ///
        /// Initializes a new instance of Address.
        ///
        public Address()
        {

        }
    }
}

Wir müssen in diesem Fall eine zusätzliche Eigenschaft erstellen, die uns in der Datenbank die Beziehung darstellen lässt. Werden dann die Tabellen durch den DbContext generiert, so sieht die erstellte Datenbank und deren Tabellen wie folgt aus:

2013-09-19_Entity_Framework_Blogpost_01

Soweit alles gut, wir hoben die Anforderung erfüllt, dass einem Benutzer mehrere Adressen hinzugefügt werden können.

Automatische Generierung

Manchmal ist es ja ganz nett, wenn alles vollautomatisch gehen würde. In diesem Fall reicht nur die Wahl des entsprechenden Auflistungs-Typen. Damit Entity Framework die Beziehungen automatisch generiert, muss die Klasse Person nur wie folgt angepasst werden:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;

namespace AddressManager.Models
{
     ///
     /// Model class for a person.
     ///

     public class Person : PersistanceBase
     {
     ///
     /// Get's or set's the value for Firstname.
     ///
    [Required]
    public string Firstname { get; set; }
    ///
    /// Get's or set's the value for Lastname.
    ///
    [Required]
    public string Lastname { get; set; }
    ///
    /// Get's or set's the value for DateOfBirth.
    ///
    [DataType("datetime2")]
    public DateTime DateOfBirth { get; set; }
    ///
    /// Get's or set's the value for the assigned
    /// Addresses.
    ///
    public virtual ICollection<Address>Addresses { get; set; }
    ///
    /// Get's or set's the value for the assigned
    /// Phones.
    ///public virtual ICollection Phones { get; set; }
    ///
    /// Initializes a new instance of Person.
    ///
    public Person()
    {

    }
  }
}

Wie macht es eigentlich der Entity Framework Designer?

Genau gleich wie das oben beschrieben wurde. Es lohnt sich zuweilen bei den generierten Klassen von Microsoft nachzuschauen wie sie es machen. Natürlich ist nicht alles Gold was glänzt, aber man erspart sich viel Arbeit und Kopfzerbrechen.

Wir haben die IEnumerable Schnittstelle durch die Schnittstelle ICollection ersetzt. Lassen wir nun den DbContext unsere Datenbank generieren, so werden die Tabellen wie folgt erstellt:

2013-09-19_Entity_Framework_Blogpost_02

Der Klasse Adresse konnten wir die zusätzliche Eigenschaft, die wir notabene nur für die Beziehungsdarstellung zwischen Person und Adresse verwendet haben, entfernt werden.

Mein Fazit

Arbeite ich mit Entity Framework und Code-first, so werde ich in Zukunft immer anstelle der IEnumerable Schnittstelle die ICollection verwenden, da ich dann meinen Code, meiner Ansicht nach, nicht mit SQL spezifischen Attributen versehen muss, nur damit mir eine Beziehung erstellt wird.

Für Anregungen, Kritik aber auch Lob danke ich bereits im Voraus.
kick it on dotnet-kicks.de

Reverse Engineering mit Visual Studio 2010

Wenn man sich gewöhnt ist zuerst Klassen zu erstellen und dann ein Model haben möchte, dann kann man schon ein wenig in Verzweiflung versinken. Nun ist es aber mit den Tools:

    • Visual Studio 2010 Ultimate
    • Visual Studio 2010 Feature Pack

möglich bereits erstellten Code rückwärts zu modellieren. Damit wir das Ganze Schritt für Schritt durchführen können, ist dieser Betrag wie folgt unterteilt:

        1. Erstellung einer Dummy Lösung mit einem manuell erstellten Klassenmodell
        2. Erstellen eins ModellProjektes

Erstellung des Beispiel Projektes

Wir halten es ganz einfach und wählen in den Projektvorlagen eine Windows Class Library aus. Wir fügen folgende Klassen hinzu:

        • Person
        • Address
        • Phone
        • PhoneType

Schlussendlich sieht das Diagramm dann wie folgt aus:

Klassen Diagramm mit dem Visual Studio Standard Generator

Nun müssen wird das Build-Directory anpassen, dass kann in ein x-beliebiges Verzeichnis sein, aber nur für das Beispiel hier.

Output Directory des Builds.

Als nächster Schritt erstellen wir uns ein Modell Projekt.

Das Modell Projekt

Nun wählen wir aus den Projektvorlagen, die Vorlage aus dem Bereich Modelling und erstellen uns eine leere Lösung. Danach fügen wir uns ein Klassendiagramm über den entsprechenden Menüpunkt der Solution hinzu.

Nun müssen wir dafür sorgen dass der Architektur Explorer ersichtlich ist. Ist dieser nicht auffindbar, kann man ihn wie folgt sichtbar machen:

  1. Auswahl Menu-Option Architektur
  2. Auswahl Fenster /Windows
  3. Selektion des Architektur Explorer

Architektur Explorer zum Vorschein bringen.

Der Architektur Explorer ist dann am unteren Rand von Visual Studio ersichtlich.

Architektur Explorer

Damit wir nun auch das vorher erstellte Projekt importieren können gehen wir wie folgt vor:

  1. Option Select Files… wählen
  2. Im öffnenden Explorer Fenster zum Pfad hinnavigieren, welchen wir vorhing bei der Beispiel DLL als Output Verzeichnis angegeben haben such und die DLL auswählen.

Nachdem wir unsere DLL selektiert haben sieht der Architecture Explorer so aus:

Architektur Explorer nach Auswahl unseres Beispielprojektes

Nun können wir beginnen die Klassen in ein Klassen Diagramm des Modelprojektes reinzuziehen und haben unser „Reverse Engineering“ erfolgreich durchgefüht.

Importierte Klassen im Klassendiagramm eines Modell Projektes

Nun können wir die notwendigen Anpassungen, falls wir mit unserem Modell nicht ganz zufrieden sind, vornehmen und den Code wieder generieren lassen.

Fazit

Es stellt sich nun die Frage, waren meine Google Anfragen so unpräzise oder ist es so einfach dass man es nicht wirklich dokumentieren braucht? Nun ich hoffe trotz der Trivialität des Vorgangs, dem einen oder anderen ein wenig Zeit mit der Suche im Web erspart zu haben.

Für Anregungen und Kritik bin ich offen. Hat der Beitrag gefallen, so würde ich mich über einen Kick freuen.

kick it on dotnet-kicks.de

Schritt für Schritt zum eigenen generischen Repository

Das Speichern und Abfragen von Daten wäre mit dem klassischen Repository-Pattern viel zu aufwändig für kleinere Projekte. In Anlehnung an das hier, in Verbindung mit dem Unit Of Work Pattern, verwendete generische Repository, habe ich mir überlegt, dass es doch für kleinere Projekte auch eine Lösung geben sollte, die nicht so viel Aufwand nach sich zieht.

Als Grundlage, oder besser gesagt als Ausgangspunkt habe ich mir diesen Artikel zur Brust genommen.

Wie wollen wir vorgehen?

  1. Wir erstellen uns ein ganz triviales Model (Adressverwaltung)
  2. Dann erstellen wir uns Schritt für Schritt das Repository
  3. Am Schluss ziehen wir noch ein Fazit aus dem Artikel
    1. Erstellung des Models

      In Anbetracht, dass nicht zwingend die Korrektheit der Datenmodellierung und der verwendeten Typen im Mittelpunkt steht, erstellen wir uns das Modell wie folgt.

      Das Modell unserer Adressverwaltung

      Wichtig an diesem Punkt ist, dass die Pluralisierung nicht geändert wird (hierzu später mehr bei der Erstellung des Repositories). Die Eigenschaften für die Personen-Entität sind hier represäntativ für alle anderen auch.

      Das Modell unserer Adressverwaltung

      Eine Spezialität ist die komplexe Eigenschaft Persistance, welch die Attribute für

      • CreateDate
      • ModifyDate
      • Creator
      • Modifier

      beherbergt. Schön wäre es ja, wenn alle Attribute bei der entsprechenden Operation des Repositories, Add, Update automatisch aktualisiert wird (dazu später mehr).

      Soweit so gut, wir haben unsere Modell für die 1001 Adressverwaltung erstellt. Wollen wir doch zum nächsten Schritt übergehen, die Erstellung des Repositories.

      Die Repository Erstellung

      Damit wir mit unserem Repository die CRUD Operation unterstützen können brauchen wir folgende Methoden:

      • Rumpf des Repositories
      • Add
      • Remove
      • Update
      • GetObjectById
      • GetObjectByQuery
      • Persistance Attribute automatisch erstellen /aktualisieren
      Rumpf des Repositories

      Damit wir die restlichen Methoden implementieren können müssen wir das Repository wie folgt erstellen:

      namespace GenericRepositoryPattern
      {
          public class Repository<T> : IDisposable where T: EntityObject
          {
              /// <summary>
              /// The context of the database.
              /// </summary>
              private readonly AddressModelContainer context;
              /// <summary>
              /// The related objectSet to the context. Is dependent
              /// of the context.
              /// </summary>
              private readonly ObjectSet<T> objectSet;
              /// <summary>
              /// Constructor to initialize the context and the
              /// objectSet.
              /// </summary>
              /// <param name="context">The context of the model.</param>
              public Repository(AddressModelContainer context)
              {
                  this.context = context;
      
                  if (context != null) 
                  { 
                      this.objectSet = context.CreateObjectSet<T>();
                      this.context.SavingChanges +=new EventHandler(context_SavingChanges);
                  }
              }
      

      (Anmerkung: Die Wiedergabe des Code’s ist gekürzt).
      Wichtig hierbei ist, dass der Context von aussen her dem Konstruktor übergeben werden muss und das ObjectSet vom instantiierten Context das ObjectSet erhält.

      Add Methode

      Damit wir ein neues Objekt zu unserem ObjectContex hinzufügen können müssen wir die Methode wie folgt implementieren.

              /// <summary>
              /// Add's a newly created entity to the
              /// objectSet
              /// </summary>
              /// <param name="entity">The newly created entity.</param>
              public void Add(T entity)
              {
                  this.objectSet.AddObject(entity);            
                  this.context.SaveChanges();                    
              }
      

      Also eine ganz einfache Sache. Als nächstes wenden wir uns der Remove Methode zu.

      Remove Methode

      Das Entfernen eines Objektes ist relativ rasch implementiert:

              public void Remove(T entity)
              {
                  if (entity != null)
                  {
                      this.objectSet.Attach(entity);
                      this.context.DeleteObject(entity);
                      this.context.SaveChanges();
                  }
              }
      

      Also keine Hexerei. Als nächstes wenden wir uns der Update Methode zu und schauen diese genauer an.

      Update Methode

      Die Update Methode zeigt sich in ihrer Ausprägung so:

              public void Update(T entity)
              {
                  this.objectSet.Attach(entity);
                  this.context.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);        
                  this.context.SaveChanges();
              }
      

      Damit der Context weiss, dass sich was geändert hat muss die Entität an den Context wieder „Attached“ und im ObjectStateManager der EntityState auf Modified gestellt werden. Dann speichert der Context die Daten aktualisiert.

      GetObjectById

      Damit wir ein Object vom Typ Person erhalten, anhand der übergebenen Id, erstellen wir uns in der Methode einen EntityKey und einem KeyValuePair. Das Attribut dass die Id verkörpert heiss auch „Id“.

              public T GetById(Int32 id)
              {
                  string entitySet = string.Format(@"{0}Set", typeof(T).Name);
                  KeyValuePair<string, object> member = new KeyValuePair<string, object>("Id", id);
                  IEnumerable<KeyValuePair<string, object>> values = new KeyValuePair<string, object>[] { member };
      
                  EntityKey key = new EntityKey(entitySet, values);
      
                  return (T)this.context.GetObjectByKey(key);
              }
      

      Und hier sehen wir auch wieso wir die Pluralisierung PersonSet belassen haben. Damit der Context weiss in welchem Set er suchen soll müssen wir dieses übergeben. Das erreichen wird mit dem kleinen Codeschnipsel

      string entitySet = string.Format(@"{0}Set", typeof(T).Name);
      

      Danach wird der EntityKey erstellt und dem Context zur Abfrage übergeben. Das gefundene Object casten wird dann in den generischen Typ den wir übergeben haben.

      GetObjectByQuery

      Damit wir spezifisch ein Objekt, oder auch mehrere zurück erhalten können wir diese mit dem entsprechenden Methode und einer Func zurück holen. Als Beispiel: Wenn ich eine Person haben möchte bei welchem ich nur den Vornahmen und den Nachnamen haben möchte, dann lege ich beim Aufrufer der Methode folgende Funktion fest:

      Func<Person, bool> functionPerson = person => person.FirstName.ToLower().Equals("hans");
      

      Die kann dann der Methode übergeben werden und diese erstellt dann auch das Resultat anhand der übergebenen Methode.

              public IEnumerable<T> GetObjectByQuery(Func<T, bool> customQuery)
              {
                  if (customQuery == null)
                  {
                      throw new ArgumentNullException("parameter: customquery must be given by caller!");
                  }
      
                  List<T> result = this.objectSet.Where(customQuery).ToList();
                  result.ForEach(this.objectSet.Detach);
                  return result;
              }
      [/csharp]
      <p>Wichtig ist nur, dass die gefundenen Objekte vom Context getrennt werden, damit diese auch ausserhalb der Verwendung des Contextes verwendet werden können.</p>
      <h6>Persistance Attribute aktualisieren</h6>
      <p>Dem Leser wird nicht entgangen sein, dass im Konstruktor der Context mit seinem Ereignis SavingChanges verknüpft worden ist. Hier werden alle relevanten Attribute vom komplexen Property IPersistance aktualisiert oder erstellt. Die Ereignisbehandlung sieht dann so aus:</p>
      
              void context_SavingChanges(object sender, EventArgs e)
              {
                  this.context.ObjectStateManager.GetObjectStateEntries(EntityState.Added).ToList().ForEach(entity => {
                      if (entity != null && entity.Entity != null && entity.Entity.GetType().GetProperty("Persistance") != null)
                      {
                          IPersistance persistance = CreatePersistance();
                          entity.Entity.GetType().GetProperty("Persistance").SetValue(entity.Entity, persistance, null);
                      }
                  });
      
                  this.context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified).ToList().ForEach(entity =>
                  {
                      if (entity != null && entity.Entity != null && entity.Entity.GetType().GetProperty("Persistance") != null)
                      {
                          UpdatePersistance(entity.Entity);
                      }
                  });
              }
      

      Hier wird eigentlich nicht’s anderes gemacht, als das alle Entitäten die den Status Added oder Modified haben das Persistance komplexe Property angepasst. Für das sind dann die zwei Methoden zuständig.

              private IPersistance CreatePersistance()
              {
                  IPersistance persistance = new IPersistance();
                  persistance.CreateDate = DateTime.Now;
                  persistance.ModifyDate = DateTime.Now;
                  persistance.Creator = WindowsIdentity.GetCurrent().Name;
                  persistance.Modifier = persistance.Creator;
                  return persistance;
              }
      

      Wird verwendet wenn eine neue Entität persistiert wird und die nachfolgende wenn eine Entität geändert worden ist.

              private void UpdatePersistance(object entity)
              {
                  if (typeof(T).GetProperty("Persistance") != null)
                  {
                      IPersistance persistance = (IPersistance)typeof(T).GetProperty("Persistance").GetValue(entity, null);
                      if (persistance != null)
                      {
                          persistance.Modifier = WindowsIdentity.GetCurrent().Name;
                          persistance.ModifyDate = DateTime.Now;
                      }
                  }
              }
      

      Zur Übersicht noch der vollständige Code des gesamten Repositories.

      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Text;
      using System.Data.Objects.DataClasses;
      using GenericRepositoryPattern.Model;
      using System.Data.Objects;
      using System.Data;
      using System.Security.Principal;
      using System.Reflection;
      
      namespace GenericRepositoryPattern
      {
          /// <summary>
          /// Generic repository.
          /// </summary>
          /// <typeparam name="T">For example it could be a model entity Person</typeparam>
          public class Repository<T> : IDisposable where T: EntityObject
          {
              /// <summary>
              /// The context of the database.
              /// </summary>
              private readonly AddressModelContainer context;
              /// <summary>
              /// The related objectSet to the context. Is dependent
              /// of the context.
              /// </summary>
              private readonly ObjectSet<T> objectSet;
              /// <summary>
              /// Constructor to initialize the context and the
              /// objectSet.
              /// </summary>
              /// <param name="context">The context of the model.</param>
              public Repository(AddressModelContainer context)
              {
                  this.context = context;
      
                  if (context != null) 
                  { 
                      this.objectSet = context.CreateObjectSet<T>();
                      this.context.SavingChanges +=new EventHandler(context_SavingChanges);
                  }
              }
              /// <summary>
              /// Get's a specific entity by it's key.
              /// </summary>
              /// <param name="id">The id from the object to retrieve.</param>
              /// <returns>The object if it's present in the db, or NULL if not existent.</returns>
              public T GetById(Int32 id)
              {
                  string entitySet = string.Format(@"{0}Set", typeof(T).Name);
                  KeyValuePair<string, object> member = new KeyValuePair<string, object>("Id", id);
                  IEnumerable<KeyValuePair<string, object>> values = new KeyValuePair<string, object>[] { member };
      
                  EntityKey key = new EntityKey(entitySet, values);
      
                  return (T)this.context.GetObjectByKey(key);
              }
              /// <summary>
              /// Allows the user to query the db with a custom
              /// function to retrieve data from the specific type
              /// of entity.
              /// </summary>
              /// <param name="customQuery">Created outisde by caller</param>
              /// <returns>A queryable of T with the possibility to treat it further.</returns>
              /// <example>
              /// Func<Person, bool> functionPerson = person => person.FirstName.ToLower().Contains("da")
              /// </example>
              public IEnumerable<T> GetObjectByQuery(Func<T, bool> customQuery)
              {
                  if (customQuery == null)
                  {
                      throw new ArgumentNullException("parameter: customquery must be given by caller!");
                  }
      
                  List<T> result = this.objectSet.Where(customQuery).ToList();
                  result.ForEach(this.objectSet.Detach);
                  return result;
              }
      
              /// <summary>
              /// Updates the values of the entity.
              /// </summary>
              /// <param name="entity">Updates a given entity with new values.</param>
              public void Update(T entity)
              {
                  this.objectSet.Attach(entity);
                  this.context.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);        
                  this.context.SaveChanges();
              }
              /// <summary>
              /// Add's a newly created entity to the
              /// objectSet
              /// </summary>
              /// <param name="entity">The newly created entity.</param>
              public void Add(T entity)
              {
                  this.objectSet.AddObject(entity);            
                  this.context.SaveChanges();                    
              }
              /// <summary>
              /// Occurs every time an entity is saved or updated.
              /// It goes through the entity properties to update or craete
              /// the persistance attributes that are necessary for saving.
              /// </summary>
              /// <param name="sender">The context</param>
              /// <param name="e">The eventargs from the context.</param>
              void context_SavingChanges(object sender, EventArgs e)
              {
                  this.context.ObjectStateManager.GetObjectStateEntries(EntityState.Added).ToList().ForEach(entity => {
                      if (entity != null && entity.Entity != null && entity.Entity.GetType().GetProperty("Persistance") != null)
                      {
                          IPersistance persistance = CreatePersistance();
                          entity.Entity.GetType().GetProperty("Persistance").SetValue(entity.Entity, persistance, null);
                      }
                  });
      
                  this.context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified).ToList().ForEach(entity =>
                  {
                      if (entity != null && entity.Entity != null && entity.Entity.GetType().GetProperty("Persistance") != null)
                      {
                          UpdatePersistance(entity.Entity);
                      }
                  });
              }
              /// <summary>
              /// Set's the createdate to the datetime
              /// where the creation of the entity
              /// object has been executed.
              /// </summary>
              /// <param name="entity">The entity to create the IPersistance creator and update attributes.</param>
              private IPersistance CreatePersistance()
              {
                  IPersistance persistance = new IPersistance();
                  persistance.CreateDate = DateTime.Now;
                  persistance.ModifyDate = DateTime.Now;
                  persistance.Creator = WindowsIdentity.GetCurrent().Name;
                  persistance.Modifier = persistance.Creator;
                  return persistance;
              }
              /// <summary>
              /// Set's the modifydate to the datetime
              /// where the modification of the entity
              /// object has been executed.
              /// </summary>
              /// <param name="entity">The entity to update the IPersistance modify attributes.</param>
              private void UpdatePersistance(object entity)
              {
                  if (typeof(T).GetProperty("Persistance") != null)
                  {
                      IPersistance persistance = (IPersistance)typeof(T).GetProperty("Persistance").GetValue(entity, null);
                      if (persistance != null)
                      {
                          persistance.Modifier = WindowsIdentity.GetCurrent().Name;
                          persistance.ModifyDate = DateTime.Now;
                      }
                  }
              }
              /// <summary>
              /// Removes an entity from the database.
              /// </summary>
              /// <param name="entity">The entity to remove. Must be loaded prior to remove.</param>
              public void Remove(T entity)
              {
                  if (entity != null)
                  {
                      this.objectSet.Attach(entity);
                      this.context.DeleteObject(entity);
                      this.context.SaveChanges();
                  }
              }
      
              #region IDisposable Members
              /// <summary>
              /// Dispose the context.
              /// </summary>
              public void Dispose()
              {
                  if (this.context != null)
                  {
                      this.context.Dispose();
                  }
              }
      
              #endregion
          }
      }
      

      Fazit

      Mit dem hier vorgestellten Ansatz ist es uns , auch für kleinere Projekte möglich, rasch und einfach CRUD Methoden zur Verfügung zu stellen ohne, dass wir das Unit of Work und das Repositoriy Pattern in ihren grössten Ausprägungen implementieren müssen.

      Wenn Ihnen der Artikel gefallen hat, würde ich mich über einen KICK freuen. Auf Verbesserungsvorschläge und Kritik bin ich natürlich auch offen.

      kick it on dotnet-kicks.de

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

Folgen

Erhalte jeden neuen Beitrag in deinen Posteingang.