Microsoft Unity 2.0 – Einstieg leicht gemacht

Statische Referenzen auf Assemblies haben Vor- und Nachteile. Wer im Zeitalter von dynamischen Systemen und verteilten Anwendungen flexibel sein möchte nutzt oft Dependency Injection Frameworks. Unity ist so eines.

Unity befindet sich aktuell in Version 2.0. Hier findet Ihr den Download: http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=9093

In einem neuen Projekt habe ich die Aufgabe gestellt bekommen, ein manndantenfähiges Webservice zu entwickeln, welches unabhängig vom Manndanten, bestimmte Methoden zur Verfügung stellen soll. Es soll Methoden geben, welche für alle Manndanten gleich sind, aber auch Spezialimplementierungen für einen bestimmten Manndanten zulässt.

Nachdem ich möglichst flexibel sein wollte, habe ich mir folgende Architektur überlegt:

Wie man sieht, kennt das Webservice lediglich die Verträge/Interfaces. Unity soll für mich entscheiden, welche Implementierung (KundeA, KundeB, KundeDefault) „ziehen“ soll. Man könnte natürlich auch noch KundeA und KundeB (die Spezialimplementierungen) auf KundeDefault referenzieren lassen, habe ich aber vorläufig nicht gemacht.

Um Unity einzubinden, muss man dem ausführenden Projekt (in meinem Fall „WcfService“) folgende Assemblies hinzufügen: Microsoft.Practices.Unity und Microsoft.Practices.Unity.Configuration.

In der web.config habe ich eine eigene Section eingerichtet und lasse diese auf die Datei Unitiy.config verweisen:

[sourcecode language=“xml“]
<configSections>
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration" />
</configSections>
<unity configSource="Unity.config" />
[/sourcecode]

In der Unity.config habe ich folgende Zeilen hinzugefügt:

[sourcecode language=“xml“]
<?xml version="1.0" encoding="utf-8" ?>
<unity>
<typeAliases>

<typeAlias alias="IVerkaufschance"
type="Contracts.IVerkaufschance, Contracts" />

<typeAlias alias="VerkaufschanceKundeDefault"
type="KundeDefault.Verkaufschance, KundeDefault" />

<typeAlias alias="VerkaufschanceKundeA"
type="KundeA.Verkaufschance, KundeA" />

<typeAlias alias="VerkaufschanceKundeB"
type="KundeB.Verkaufschance, KundeB" />

</typeAliases>

<containers>
<container>
<types>

<!– Default (un-named) mapping for IVerkaufschance to VerkaufschanceKundeA –>
<type type="IVerkaufschance" mapTo="VerkaufschanceKundeDefault"></type>

<!– Named mapping for IVerkaufschance to VerkaufschanceKundeA –>
<type type="IVerkaufschance" mapTo="VerkaufschanceKundeA" name="KundeA"></type>

<!– Named mapping for IVerkaufschance to VerkaufschanceKundeB –>
<type type="IVerkaufschance" mapTo="VerkaufschanceKundeB" name="KundeB"></type>

</types>
</container>
</containers>
</unity>
[/sourcecode]

Wie man sieht, habe ich drei Typen angelegt. Die Default-Implementierung (un-named mapping, hier fehlt das „name“-Attribut) für den Vertrag „IVerkaufschance“ und die Spezialimplementierungen für KundeA und KundeB (named mappings). Zudem muss man natürlich die Typen für Vertrag und Implementierungen bekannt geben, dies geschieht über die TypeAliases.

Wie man im Architektur-Diagramm erkennt, hat das WcfService keine Kenntnis über die Implementierungen für Default, KundeA und KundeB. Daher ist es notwendig, in den jeweiligen Projekten, im Post-Build-Event die erzeugte Assembly in das bin-Verzeichnis der ausführenden Applikation (WcfService) zu kopieren. Das geht am einfachsten mittels XCOPY

xcopy "$(SolutionDir)KundeAbindebugKundeA.dll" "$(SolutionDir)WcfServicebin" /Y

Im Code müsste man nun die Unity-Container (in meinem Fall gibt es nur einen) nach den entsprechenden Typen durchsuchen. Um das zu vereinfachen, habe ich eine Helperklasse geschrieben:

[sourcecode language=“csharp“]
public interface IContainerAccessor
{
IUnityContainer Container { get; }
}

public class Dependency
{
public static void Register(IUnityContainer container)
{
UnityConfigurationSection config = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
config.Containers.Default.Configure(container);
}

public static T Resolve<T>()
{
return Container.Resolve<T>();
}

public static T Resolve<T>(object name)
{
try
{
return Container.Resolve<T>(name.ToString());
}
catch (ResolutionFailedException)
{
return Container.Resolve<T>();
}
}

private static IUnityContainer _container;
public static IUnityContainer Container
{
get
{
// Testing only – see Set method below
if (_container != null) return _container;

var context = HttpContext.Current;

// The most likely situation where this occurs is because we are on a different thread. These threads
// are not part of the ‚current‘ context, hence explosion
if (context == null)
{
try
{
IUnityContainer c = new UnityContainer();
Register(c);
return c;
}
catch
{
// When running from the TEST projects, the above fails.
return null;
}
}

// Get it from global. This is cached in the Application, so is the ideal scenario
var accessor = context.ApplicationInstance as IContainerAccessor;
if (accessor == null)
{
return null;
}

var container = accessor.Container;

if (container == null)
{
throw new InvalidOperationException("No Unity container found");
}

return container;
}

// Only for testing purposes
set
{
_container = value;
}
}

}
[/sourcecode]

Meinem Webserviceprojekt (WcfService) habe ich nun ein ein Wcf Service hinzugefügt, welches ich „Verkaufschance“ benannt habe. Im generierten Code der Verkaufschance.svc.cs-Klasse habe das Interface auf „contracts.IVerkaufschance“ abgeändert und das generierte Interface weggeschmissen und die Copy-Funktion, welche im Interface definiert ist, implementiert.

[sourcecode language=“csharp“]
// NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "Verkaufschance" in code, svc and config file together.
public class Verkaufschance : contracts.IVerkaufschance
{
public string Copy(string organization)
{
return Dependency.Resolve<contracts.IVerkaufschance>(organization).Copy("something");
}
}
[/sourcecode]

In der Copy-Funktkion kommt nun Unity zum Einsatz. Mittels Dependency.Resolve löse ich gegen die erzeugte Konfiguration (Unity.config) die entsprechende Assembly auf. Der Parameter „organization“ ist der Schlüssel zu den Named-Mappings. Findet er den Schlüssel nicht, zieht die Default-Implementierung.

Die Implementierung, für KundeA, sieht dann wie folgt aus:

[sourcecode language=“csharp“]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace KundeA
{
class Verkaufschance : Contracts.IVerkaufschance
{
#region IVerkaufschance Members

public string Copy(string organization)
{
return "Sonderlösung für " + organization;
}

#endregion
}
}
[/sourcecode]

Ich habe somit einen Vertrag (im Contracts-Projekt), welchen ich für die Implementierung des Webservices und für die eigentliche Implementierung der Methoden verwende. So ist sichergestellt, dass die Implementierung für jeden Manndanten gleich ist bzw. die gleichen Methoden zur Verfügung stellt.

Ich hoffe ich konnte euch damit den Einstieg in Unity etwas erleichtern.

Cheers
Chris

Leave a reply