Muster: Dependency-Injection für Dekorierer bei Castle
Version: 1.0
.NET Version: .NET, .NET Framework
Ein Dekorierer inerweitert die Funktionalität einer Klasse. Ein typisches Beispiel ist ein Gateway, welches Daten von einem anderen Service liest und um Caching erweitert werden soll. Die Implementierung eines Decorators in C# ist prinipiell leicht, daher wird in diesem Muster beschrieben, wie dieses in Schleupen.CS 3.0 mit Castle umgesetzt werden kann.
Design
Das prinzipielle Design ist in Decorators beschrieben.
Implementierung
Wir betrachten das Beispiel eines Gateways. Die Schnittstelle habe dabei folgende Gestalt:
public interface IBestellAuftragEntityServiceGateway
{
Task<BestellAuftrag> QueryAsync(BestellAuftragId bestellAuftragId);
...
}
Analog sei die Implementierung wie folgt gegeben:
public class BestellAuftragEntityServiceGateway : IBestellAuftragEntityServiceGateway
{
...
public async Task<BestellAuftrag> QueryAsync(BestellAuftragId bestellAuftragId)
{
if (bestellAuftragId == null) { return null;}
using IBestellAuftragEntityServiceClient serviceClient = serviceClientFactory.Create();
QueryByIdResponse requestResult = await serviceClient.QueryByIdAsync(new QueryByIdRequest(null, new ArrayOfBestellAuftragId(bestellAuftragId.Value)));
BestellAuftrag bestellAuftrag = bestellAuftragAssembler.ToDomainObject(requestResult.BestellAuftragListe.First());
return bestellAuftrag;
}
Wir implementieren nun einen Dekorierer, in dem wir per Konstruktor-Injection das eigentliche Gateway entgegennehmen (nicht erkennbar am Typ, da eine Schnittstelle übergeben wird).
public class CachingBestellAuftragEntityServiceGateway : IBestellAuftragEntityServiceGateway
{
private readonly IBestellAuftragEntityServiceGateway bestellAuftragEntityServiceGateway;
...
public CachingBestellAuftragEntityServiceGateway(IBestellAuftragEntityServiceGateway bestellAuftragEntityServiceGateway, ...)
{
this.bestellAuftragEntityServiceGateway = bestellAuftragEntityServiceGateway ?? throw new ArgumentNullException(nameof(bestellAuftragEntityServiceGateway));
...
}
public async Task<BestellAuftrag> QueryAsync(BestellAuftragId bestellAuftragId)
{
if (bestellAuftragId == null) { return null; }
return await bestellAuftragCache.GetOrSetAsync(
bestellAuftragId,
bestellAuftragEntityServiceGateway.QueryAsync);
}
Die Konfiguration, dass Castle die Typen wie erwartet zusammensetzt, ist recht einfach. Da der WcfContainerConfigurator mithilfe der Property Assemblies bereits nach allen Typen, die der Namenskonvention entsprechen, sucht, muss das CachingBestellAuftragEntityServiceGateway gefiltert werden. Dieses muss die Default-Implementierung sein: Hierzu wird dieser Type per windsorContainer.Register(...) registriert.
Für windsorContainer.Register(...) muss der Typ per string aufgelöst werden, da dieser nur per Laufzeit und nicht per Compile-Zeit zur Verfügung steht.
public class ServiceHostFactoryConfigurator : WcfContainerConfigurator
{
protected override IEnumerable<Assembly> Assemblies
{
get
{
yield return Assembly.Load(AssemblyNames.Services);
yield return Assembly.Load(AssemblyNames.Contracts);
yield return Assembly.Load(AssemblyNames.BusinessCases);
yield return Assembly.Load(AssemblyNames.Repositories);
yield return Assembly.Load(AssemblyNames.Gateways);
yield return Assembly.Load(AssemblyNames.GatewaysContracts);
}
}
protected virtual bool IsInsideWcfOperation => true;
...
protected override void OnContainerConfiguring(IWindsorContainer windsorContainer)
{
windsorContainer.Register(Component.For<IBestellAuftragEntityServiceGateway>()
.ImplementedBy(ResolveType("Schleupen.AS.MT.BIB.Bestellungen.Auftraege.EntityService.V3_1.Caching.CachingBestellAuftragEntityServiceGateway", AssemblyNames.Gateways))
.IsDefault());
base.OnContainerConfiguring(windsorContainer);
}
protected override bool UseType(Type type)
{
...
if (type.Name == "CachingBestellAuftragEntityServiceGateway") { return false; }
return base.UseType(type);
}
protected Type ResolveType(string fullTypeName, string assemblyName)
{
return Assembly.Load(assemblyName)
.GetTypes()
.Single(t => t.FullName == fullTypeName);
}
protected override void SetupWiring(Assembly assembly, BasedOnDescriptor descriptor)
{
if (descriptor == null) { throw new ArgumentNullException(nameof(descriptor)); }
descriptor.WithServiceAllInterfaces();
}
Um zu prüfen, ob die Registrierung wie erwartet funktioniert, testen wir dies nun.
Unittest
Den Test implementieren wir einfach im GenericServiceTest, in dem generell das Zusammenstellen der Objekte getestet wird.
[TestFixture]
internal sealed class GenericServiceTest : GenericServiceTestBase
{
...
[Test]
public void Resolve_CachingBestellAuftragEntityServiceGateway_ShouldReturnDecorator()
{
DependencyInjectionContainer diContainer = CreateDependencyInjectionContainer();
using IDisposable scope = diContainer.Windsor.BeginScope();
ContainerConfigurator.Configure(diContainer.Windsor);
IBestellAuftragEntityServiceGateway gateway = diContainer.Windsor.Resolve<IBestellAuftragEntityServiceGateway>();
Assert.That(gateway.GetType().Name, Is.EqualTo("CachingBestellAuftragEntityServiceGateway"));
}
Abgrenzung
Das Muster zeigt nicht das Dekorierer-Muster der Gang of Four an sich, sondern nur die Registrierung.
Stärken und Schwächen
Stärken
- Kapselung
Schwächen
- Nicht so explizites Design