Renare tjänsteimplementation med Enterprise Library

I min förra post skrev jag hur vi med Unity kan löskoppla tjänsteimplementationens beroenden på ett smidigt sätt. Unity kommer med Enterprise Library från v4.0 alternativ som en separat nedladdning.

I April 2007 (snart 2år!) släpptes v3 av EntLib och introducerade då Validation Application Block samt nya funktioner i Exception Handling Application Block för bland annat integration med WCF. Jag känner vad du tänker; 2-3år gamla pryttlar, kan han inte skriva något fancy om WCF4 istället?! . Faktum är att jag ideligen möts av hakor i bordskivan när jag visar hur dessa två applikationsblock kan användas tillsammans med WCF.

Problemet

Nedan har jag skrivit ett exempel på hur det ofta kan se ut. Tjänsten, som erbjuder möjligheten att beställa motorcyklar, innehåller två rader kod som egentligen är de enda som vi vill behöva bry oss om. Övrig kod i tjänsteimplementationen är kod som berör validering, felhantering och loggning.

Mycket av koden som beror validering, felhantering och loggning vill vi så långt som möjligt bryta ut och kunna återanvända på lämpligt vis samt att vi vill slippa se den ihop med vår affärslogik. Den tillför inget värde i detta sammanhang utan skapar bara ett onödigt brus.

 public class SmellyMotorbikeOrderService : IMotorbikeOrderService {

    public MotorbikeOrderResult SubmitOrder(MotorbikeOrder order) {

        if (order == null) {
            throw new FaultException("The order was equal to null.");
        }

        if (order.Quantity < 1) {
            throw new FaultException("Quantity must be greater than zero.");
        }
        if (order.Brand != "Ducati") {
            throw new FaultException("We only stock Ducati.");
        }

        if (!(new List<string>{ "1198", "Streetfighter", "Desmosedici", "Monster" }).Contains(order.Model)) {
            throw new FaultException("The specified model doesn't exist in the Ducati product line.");
        }

        try {
            //Dessa två rader är vad vi egentligen endast vill göra.
            int orderId = SaveOrder(order);
            return new MotorbikeOrderResult() { OrderId = orderId };
        }
        catch (Exception ex){
            //logga felet som inträffade när ordern skulle sparas
            LogException(ex, order);
            throw new FaultException("An error occured while processing your order. " + 
                "Please contact customer support.");
        }
    }
}

Ingrepp #1 - Exception Shielding

Första steget är att få bort try-catch blocket som fångar eventuella ohanterade undantagsfel vid sparning av ordern. (Exception Shielding är något som alltid skall tillämpas mellan tjänstebarriärer för att inte exponera eventuella interna klasser, namn, medlemmar etc som kan komma med ett felmeddelande.)

Vi raderar try-catch blocket och adderar istället ExceptionShielding attributet på vårt tjänstekontrakt. Attributet ligger i Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WCF namespace’t

Det är kort och gott ett Contract Behavior som appliceras med hjälp av detta attribut. (IServiceBehavior implementeras också, om du hellre vill dekorera tjänsteimplementationsklassen med attributet .)

 [ServiceContract(Namespace="https://blogs.msdn.com/chrislof/samples/2009/2")]
 [ExceptionShielding] 
public interface IMotorbikeOrderService {

    [OperationContract]
    MotorbikeOrderResult SubmitOrder(MotorbikeOrder order);
}

Så, sex rader kod borta. Hur eventuella ohanterade fel från underliggande komponenters skall hanteras kan vi nu styra precis som vanligt med EntLib konfiguration. Tillägget i detta fall är att definiera vilket FaultContract vi vill returnera. Läs här för detaljer.

Ingrepp #2 - Bryt ut valideringskod

Istället för alla if-satser som validerar inkommande order i tjänsteimplementationen, så bryter vi ut detta till återanvändbara klasser i form av Validators. Dessa validatorer går sedan utmärkt att återanvända i andra sammanhang samt att de passar sig ypperligt för enhetstestning. (Återanvända och enhetstesta i samma mening - I like!)

Nedan exempel visar en av validatorerna som kontrollerar att önskad hoj faktiskt är av en modell som handlaren säljer.

 namespace EntLibWCF.Validators {

    public class ModelValidator : DomainValidator<string> {
        public ModelValidator()
            : base(null,"1198", "Streetfighter", "Desmosedici", "Monster") {
            MessageTemplate = "The specified model doesn't exist in the Ducati product line.";
        }
    }

    public class ModelValidatorAttribute : ValidatorAttribute {
        protected override Validator DoCreateValidator(Type targetType) {
            return new ModelValidator();
        }
    }
    
}

Jag skrev även en validator som kontrollerar märket på önskad hoj. Det funkar på samma sätt så jag väljer att inte visa den här. Det är egentligen bara fantasin som sätter gränser för vad man kan åstadkomma med dessa validatorer.

När validatorerna är skapade så anger vi dessa attribut på de egenskaper på MotorbikeOrder objektet (Datakontraktet) som vi vill validera.

 [DataContract(Namespace="https://blogs.msdn.com/chrislof/samples/2009/2")]
public class MotorbikeOrder : IExtensibleDataObject {

    [DataMember(Order = 0)]
    //custom
 [BrandValidator] 
    public string Brand {
        get;
        set;
    }

    [DataMember(Order = 0)]
    //custom
 [ModelValidator] 
    public string Model {
        get;
        set;
    }

    [DataMember(Order = 0)]
    //built in
 [RangeValidator(1,RangeBoundaryType.Inclusive,10,RangeBoundaryType.Inclusive)] 
    public int Quantity {
        get;
        set;
    }

    #region IExtensibleDataObject Members
}

Slutligen för att aktivera valideringen behöver vi dekorera tjänstekontraktet med attributet ValidationBehavior (Contract Behavior) samt att dekorera operationen med felkontraktet ValidationFault.

 [ServiceContract(Namespace="https://blogs.msdn.com/chrislof/samples/2009/2")]
 [ValidationBehavior] 
[ExceptionShielding]
public interface IMotorbikeOrderService {

    [OperationContract]
     [FaultContract(typeof(ValidationFault))] 
    MotorbikeOrderResult SubmitOrder([NotNullValidator] MotorbikeOrder order);
}

Resultat

Så med ovan ingrepp har vi trollat bort 18 rader kod och har endast våra två rader som faktiskt betyder något i detta sammanhang kvar. Vi har dessutom skapat oss en kodmassa som är bra mycket vänligare att underhålla.

Någon mer än jag som gillar detta?

 public class MotorbikeOrderService : IMotorbikeOrderService {
    
    public MotorbikeOrderResult SubmitOrder(MotorbikeOrder order) {
        
        //Save the order
        int orderId = SaveOrder(order);
        return new MotorbikeOrderResult() { OrderId = orderId };
    }
}

Som alltid, maila mig om du vill ha hela exempelkoden.

-c