Montag, 3. Januar 2011

AOP Advice für den EnterpriseLib 5.0 Validation Block

Das Ziel ist es, ein Domänenmodell an einer Schnittstelle zu validieren, ohne die vorhandene Schnittstellenimplementierung zu ändern. Um dieses zu bewerkstelligen, möchte ich mit Hilfe von Spring.Net und der Enterprise Library einen aspektorientierten Ansatz verfolgen.

Im vorliegenden Fall wurden Validierungsregeln mit Hilfe von ValidationAttributes aus dem Validation Block der Enterprise Library 5.0 erstellt. In diesem Framework gibt es jedoch auch die Möglichkeit Validationsregeln anstelle von Attributen per Code oder Konfiguration zu hinterlegen.

Eine fast fertige Lösung um einen AOP-Advice um eine Schnittstelle zu legen und diese automatisch alle übergebenen Argumente überprüfen zu lassen, befindet sich bereits im Namensraum Microsoft.Practices.EnterpriseLibrary.Validation.PolicyInjection der EnterpriseLibrary. Jedoch sind die dort vorhandenen Klassen zum einen ausgelegt für Microsofts Inversion of Control (IoC) Container Unity, zum anderen werden an eine Methode übergebene Parameter zwar überprüft, schlägt eine Überprüfung jedoch fehl, werden weitere Parameter nicht mehr überprüft.

Grundsätzlich sind ist die Vorgehensweise bei den IoC-Containern Unit und Spring ähnlich: die es werden Einstiegspunkte (Pointcuts) definiert, bei deren Durchlauf weitere Funktionalitäten (Aspect) ausgeführt werden. Erstellt der Container eine Instance eines Objektes, wird entsprechend der Konfiguration ein Proxy-Objekt erzeugt, welches die tatsächliche Objekt-Instanz kapselt und so in der Lage ist Aufrufe abzufangen und durch entsprechend konfigurierte Aspekte zu leiten. Dieser Ansatz wird als “Runtime Weaving” bezeichnet, da Aspekte zur Programmlaufzeit hinzugefügt/entfernt werden. Weitere Möglichkeiten anderer Frameworks (wie beispielsweise PostSharp)  sind das Einmischen von ByteCode bzw. IL-Anweisungen.

Als simplifiziertes Domänenmodell dient an dieser Stelle folgende Klasse:

    public class Costumer
    {
        [NotNullValidator]
        public string Name { get; set; }

        public int Income { get; set; }
        public virtual float Discount { get { return 0.0f; } }
    }

Das Attribut NotNullValidator soll am Ende sicherstellen, dass ein der Name zumindest auf einen leeren String gesetzt wurde.

Folgender UnitTest schlägt bis zur fertigen Implementierung fehl:

 
    [TestFixture]
    public class IntegrationTest
    {
        IService advicedService = null;
            
        [TestFixtureSetUp]
        public void Init()
        {
            IApplicationContext ctx = ContextRegistry.GetContext();
            advicedService = (IService)ctx["myService"];
        }

        [Test]
        public void ShouldBeAdvised()
        {
            PrivateCostumer pCostumer = 
				new PrivateCostumer() 
					{
						Name = null, 
	 	   				Income = 50 
					};
            Assert.That(() => advicedService.Process(pCostumer), Throws.TypeOf<ValidationException>());
        }
     }

Im TestFixtureSetUp wird Spring dazu aufgefordert, entsprechend der Konfiguration einen Kontext zu erzeugen und schließlich ein Objek mit dem Namen “myService” aus dem Kontext zu liefern. Im UnitTest (verwendet wird NUnit 2.5.x) selbst wird eine Exception erwartet. Bleibt diese aus, schlägt der Test fehl.

Die vorhandene Implementierung des Service Interfaces enthält keinerlei Abhängigkeiten zum Validation Block:

    class Service : IService
    {
        private static readonly ILog log = LogManager.GetCurrentClassLogger();

        public void Process(Costumer c)
        {
            log.Info(msg => msg("Processing Costumer {0}, {1}, {2}", c.Name, c.Income, c.Discount));
        }
    }

Damit der UnitTest nicht länger fehl schlägt, wird ein Advice implementiert, der mit Hilfe des Validation Blocks alle an eine Methode übergebenen Argumente validiert und eine Exception wirft, sollt ein Argument nicht valide sein:

using AopAlliance.Aop;
using Microsoft.Practices.EnterpriseLibrary.Validation;
using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;
using Spring.Aop;

namespace EntLibValidationInterceptor
{
    public class EntLibValidationInterceptor : IMethodBeforeAdvice, IBeforeAdvice, IAdvice
    {
        public ValidatorFactory ValidatorFactory { get; set; }

        public void Before(System.Reflection.MethodInfo method, object[] args, object target)
        {
            if(args == null)
		return; // args will be null when void methods are advised.
            Validator validator = CreateValidator();
            ValidationResults validationResults = new ValidationResults();
            
            foreach (object parameter in args)
            {
                validator.Validate(parameter, validationResults);
            }

            if (!validationResults.IsValid)
            {
                throw new ValidationException(validationResults);
            }
        }

        private Validator CreateValidator()
        {
            Validator validator = ValidatorFactory != null ? new ObjectValidator(ValidatorFactory) : new ObjectValidator();
            return validator;
        }
    }
}

Anstelle einer normalen Service-Instanz muss der Container angewiesen werden nun nur noch Instanzen auszuliefern, deren Methodenaufrufe durch den EntLibValidationInterceptor geleitet werden. Die folgende app.config weist Spring an, lediglich Proxy-Objekte auszuliefern:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="spring">
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
    </sectionGroup>
  </configSections>
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="ValidationAdvice"
              type="EntLibValidationInterceptor.EntLibValidationInterceptor, EntLibValidationInterceptor" />

      <object id="myService" type="Spring.Aop.Framework.ProxyFactoryObject">
        <property name="Target">
          <object type="EntLiValidationInterceptor.Tests.DummyDomainClasses.Service, EntLiValidationInterceptor.Tests" />
        </property>
        <property name="InterceptorNames">
          <list>
            <value>ValidationAdvice</value>
          </list>
        </property>
      </object>
    </objects>
  </spring>
</configuration>

In der vorliegenden Konfiguration werden alle Methoden der Implementierten Interfaces durch den ValidationAdvice umspannt. Es ist auch Möglich, Pointcuts nur auf bestimmte Methoden deren Name auf einen regulären Ausdruck passen zu definieren:

<object id="ValidationAdvice" type="Spring.Aop.Support.RegularExpressionMethodPointcutAdvisor, Spring.Aop">
        <property name="advice">
          <object type="EntLibValidationInterceptor.EntLibValidationInterceptor, EntLibValidationInterceptor" />
        </property>
        <property name="patterns">
          <list>
            <value>.*rocess</value>
          </list>
        </property>
      </object>

oder aber nur diejenigen Methoden mit einem Aspekt zu versehen, die ein bestimmtes Attribut, beispielsweise das BaseValidationAttribute aus dem Namensraum Microsoft.Practices.EnterpriseLibrary.Validation.Validators tragen:

<object id="ValidationAdvice" type="Spring.Aop.Support.AttributeMatchMethodPointcutAdvisor, Spring.Aop">
        <property name="advice">
          <object type="EntLibValidationInterceptor.EntLibValidationInterceptor, EntLibValidationInterceptor"/>
        </property>
        <property name="attribute" value="Microsoft.Practices.EnterpriseLibrary.Validation.Validators.BaseValidationAttribute, Microsoft.Practices.EnterpriseLibrary.Validation" />
      </object>

Update: Da hat sich doch tatsächlich ein Fehler eingeschlichen. Die object[] args sind bei advisten-Methoden ohne Parameter zur Laufzeit null. Also sollte man darauf entsprechend reagieren.

Keine Kommentare: