Work in Progress
Diese Seite ist aktuell im Review! Die Seite wurde noch nicht qualitätsgesichert und kann Fehler enthalten.
Die verlinkten Seiten sind ggf. nur für Schleupen-Mitarbeiter sichtbar.

Validierung

Dieses Dokument beschreibt das Validerungskonzept für Domänenobjekte. Die Validierung erfolgt auf Aggregatwurzelebene. Ist ein Domänenobjekt in einem Aggregat ungültig wird dadurch das ganze Aggregat invalidiert.

Design

Die Validierung findet in der Domäne statt. Dazu werden die Validierungsmethoden auf den AggregatRoot Objekten angeboten und delegieren von dort an Ihr Aggregat. Eine Beispiel findet sich in BIB (BuchEntityService, BuchausleihenActivityService und AusleihfristVerlaengernActivityService). Die Validierung wird in frühe und späte Validierung unterteilt:

Frühe Validierung

Eine frühe Validierung prüft, ob der aktuelle Datenbestand für eine Operation gültig ist (z.B. die Buchung ist gültig hinsichtlich einer durchzuführenden Abrechnung - man könnte also eine Abrechnung durchführen). Wird eine frühe Validierung benötigt, so wird zu jeder davon betroffenen Methode [DoSomething()] eine zusätzliche Methode [ValidationResult ValidateRegardingDoSomething()] eingeführt. Diese sammelt dann die entsprechenden ValidationMessage-Objekte zusammen um dem Nutzer Informationen über das wahrscheinliche Ergebnis zu liefern.

Späte Validierung

Die späte Validierung wird vor der Persistierung von Daten ausgelöst, um die Gefahr inkonsistenter Aggregate in der Datenbank zu minimieren. Existiert eine frühe Validierung, so sollte diese im Rahmen der späten Validierung genutzt werden, um Redundanzen in den Prüfungen zu vermeiden. Die späte Validierung wird sowohl durch konkrete Aktionen auf einer Entität ausgeführt, z.B. VerlaengereAusleihFrist() => ValidateRegardingVerlaengereAusleihFrist() als auch in letzter Instanz durch das Persistenzframework [ISupportsValidation.ValidateRegardingPersistence()]. Letztere Validierung ist kontextlos, kann also nur einfach Prüfungen wie z.B. Pflichtfelder durchführen.

Die Aggregatswurzeln implementieren das Interface ISupportsValidation, welches die Methode ValidateRegardingPersistence() bereitstellt. Zusätzlich implementieren die Aggregatkindknoten das Interface IAggregateChild um die Navigation in Richtung Aggregatwurzel zu ermöglichen. T ist hierbei der Typ des Parent-Objektes im Aggregat (nicht der Aggregatwurzel), die Property ParentId liefert den Primärschlüssel des Parent-Objektes. Eine genauere Beschreibung hierzu gibt es hier.

Da späte Validierung oft zusammen mit spezialisierten Ausnahmen eingesetzt wird, werden hier spezialisierte ValidationMessage-Klassen benötigt, die diese Informationen zur Verfügung stellen können.

Implementierung

Frühe Validierung

Die Methode ValidateRegardingLeiheAus() prüft, ob der Benutzer die angegebenen Bücher ausliehen kann.

[AggregateRoot]
public partial class Benutzerkonto : Entity
{
    public virtual ValidationResult ValidateRegardingLeiheAus(IEnumerable<Buch> auszuleihendeBuecher)
    {
        if (auszuleihendeBuecher == null) { throw new ArgumentNullException(nameof(auszuleihendeBuecher)); }

        ValidationResult result = ValidateAusleiheLimitNichtErreicht();

        foreach (Ausleihe existingAusleihe in ausleihen)
        {
            Buch[] bereitsAusgeliehenBuecher = existingAusleihe.SameBuecherAs(auszuleihendeBuecher).ToArray();
            foreach (var bereitsAusgeliehenesBuch in bereitsAusgeliehenBuecher)
            {
                result.Add(new BuchIstAusgeliehenValidationMessage(bereitsAusgeliehenesBuch));
            }
        }

        return result;
    }
}

Die frühe Validierung wird als in folge des Aufrufes der Validate Methode auf dem Activity Service ausgeführt.

Der Activity Service leitet in der Validate()-Methode die frühe Validierung an den Businesscase Controller weiter [buchAusleihenController.ValidateRegardingLeiheBuecherAus()].

public class BuecherAusleihenActivityService
{
    public ValidateResponse Validate(ValidateRequest request)
    {
        if (request == null) { throw new ArgumentNullException(nameof(request)); }
        if (string.IsNullOrWhiteSpace(request.BenutzerSid)) { throw new ArgumentNullException(nameof(request.BenutzerSid)); }

        IEnumerable<Buch> auszuleihendeBuecher = buchAssembler.ToDomainObject(request.Buecher);
        ValidationResult result = buchAusleihenController.ValidateRegardingLeiheBuecherAus(request.BenutzerSid, auszuleihendeBuecher);

        return new ValidateResponse { ValidationResultListe = new[] { validationResultAssembler.ToDataContract(result) }.ToList() };
    }
}

Der Businesscase Controller leitet die Validierung an die Aggregatswurzel weiter.

public class BuecherAusleihenController
{
    public ValidationResult ValidateRegardingLeiheBuecherAus(string benutzerSid, IEnumerable<Buch> buecher)
    {
        if (buecher == null) { throw new ArgumentNullException(nameof(buecher)); }
        if (string.IsNullOrEmpty(benutzerSid)) { throw new ArgumentNullException(nameof(benutzerSid)); }

        ValidationResult result = new ValidationResult();

        Benutzerkonto benutzerkonto = benutzerkontoRepository.QueryBySid(benutzerSid);
        result.Merge(benutzerkonto.ValidateRegardingLeiheAus(buecher));

        return result;
    }
}

Späte Validierung

Persistenz

Das Aggregate Root Buch implementiert ISupportsValidation, über dessen Methode ValidateRegardingPersistence() die späte Validierung durchgeführt wird. Um die ISBN zu validieren wird die Methode ValidateRegardingPersistence() auf der ISBN-Klasse aufgerufen, die ISBN-Klasse selbst implementiert ISupportsValidation nicht.

Die ISBN-Klasse ist ein Value Object und somit keine Entität, die Seite Klasse implementiert IAggregateChild, da das Parent-Objekt vom Typ Buch ist und eine Änderung auf dem Objekt dazu führen kann, dass das Aggregat als ganzes invalidiert wird.

Falls auf dem Aggregat chronologische Collections existieren, deren Werte validiert werden sollen, ist es wichtig, dass die Werte ISupportsValidation unterstützen.

[AggregateRoot]
public class Buch : Entity, ISupportsValidation
{
    public Buch(string titel, ...)
    {
        // keine Parameterprüfungen!
        // ... 
    }

    public ValidationResult ValidateRegardingPersistence() // Validierung auf Aggregatebene
    {
        ValidationResult validationResult = new ValidationResult();

        if (string.IsNullOrWhiteSpace(titel)) // Validierung auf Feldebene für jeden primitiven Type
        {
            validationResult.Add(new FieldValidationMessage(() => titel, Id, ValidationMessageLevel.Error, "Das Buch muss einen Titel definiert haben"));
        }
        // alle weiteren Felder => in Summe Validierung auf Entity / ValueObject-Ebene

        validationResult.AddRange(isbn.ValidateRegardingPersistence());

        if (inhaltsangabe != null)
        {
            validationResult.Merge(inhaltsangabe.ValidateRegardingPersistence());
        }
    }
}

public class Isbn
{
    private Regex regex = new Regex(@"...");
    private string nummer;

    public Isbn(string nummer)
    {
        this.nummer = nummer;
    }

    public ValidationResult ValidateRegardingPersistence()
    {
        ValidationResult validationResult = new ValidationResult();

        if (!string.IsNullOrWhiteSpace(nummer) && (!isbnRegex.IsMatch(nummer))
        {
            validationResult.Add(new ValidationMessage(ValidationMessageLevel.Error, "Isbn muss den Format xyz haben.")
        }

        return validationResult;
    }
}

public class Seite : Entity, IAggregateChild<Buch>
{
    private Guid parentId;

    object IAggregateChild.ParentId
    {
        get
        {
            return parentId;
        }

        set
        {
            if (value == null)
            {
                throw new ArgumentNullException(nameof(value));
            }

            if (value is Guid)
            {
                parentId = (Guid)value;
            }
            else
            {
                throw new ArgumentException("Die ParentId muss eine Guid sein");
            }
        }
    }
}

Der Entity Service leitet die Validierung an den Businesscase Controller weiter [buchVerwaltenController.Validate()].

public class BuchEntityService
{
    public ValidateResponse Validate(ValidateRequest request)
    {
        IEnumerable<Buch> buecher = buchAssembler.ToDomainObject(request.BuchListe);
        IEnumerable<ValidationResult> results = buchVerwaltenController.Validate(buecher);
        return new ValidateResponse { ValidationResultListe = validationResultAssembler.ToDataContract(results) };
    }
}

Der Businesscase Controller ruft die ValidateRegardingPersistence()-Methode auf der Aggregatwurzel auf.

public class BuchVerwaltenController
{
    public IEnumerable<ValidationResult> Validate(IEnumerable<Buch> buecher)
    {
        return buecher.Select(b => b.ValidateRegardingPersistence());
    }
}
Aktivität

Das Aggregate Root Benutzerkonto stellt eine Methode LeiheAus() bereit, durch die eine späte Validierung über die Methode ValidateRegardingLeiheAus() durchgeführt wird. Falls hier ein Fehler passiert, wird per ThrowIfInvalid() eine Exception geworfen.

[AggregateRoot]
public class Benutzerkonto : Entity, ISupportsValidation
{
    public virtual ValidationResult ValidateRegardingLeiheAus(IEnumerable<Buch> auszuleihendeBuecher)
    {
        if (auszuleihendeBuecher == null) { throw new ArgumentNullException(nameof(auszuleihendeBuecher)); }

        ValidationResult result = ValidateAusleiheLimitNichtErreicht();

        foreach (Ausleihe existingAusleihe in ausleihen)
        {
            Buch[] bereitsAusgeliehenBuecher = existingAusleihe.SameBuecherAs(auszuleihendeBuecher).ToArray();
            foreach (var bereitsAusgeliehenesBuch in bereitsAusgeliehenBuecher)
            {
                result.Add(new BuchIstAusgeliehenValidationMessage(bereitsAusgeliehenesBuch));
            }
        }

        return result;
    }

    public virtual Ausleihe LeiheAus(params Buch[] auszuleihendeBuecher)
    {
        if (auszuleihendeBuecher == null) { throw new ArgumentNullException(nameof(auszuleihendeBuecher)); }

        ValidateRegardingLeiheAus(auszuleihendeBuecher).ThrowIfInvalid();

        Ausleihe result = new Ausleihe(Guid.NewGuid(), auszuleihendeBuecher, DateTime.Now);
        ausleihen.Add(result);

        return result;
    }
}

Der Activity Service ruft die Aktion LeiheBuecherAus() auf dem Businesscase Controller auf.

public class BuecherAusleihenActivityService
{
    public ExecuteResponse Execute(ExecuteRequest request)
    {
        if (request == null) { throw new ArgumentNullException(nameof(request)); }
        if (string.IsNullOrWhiteSpace(request.BenutzerSid)) { throw new ArgumentNullException(nameof(request.BenutzerSid)); }

        Buch[] auszuleihendeBuecher = buchAssembler.ToDomainObject(request.Buecher);
        Ausleihe ausleihe = buchAusleihenController.LeiheBuecherAus(request.BenutzerSid, auszuleihendeBuecher);

        ExecuteResponse response = new ExecuteResponse();
        response.Ausleihe = ausleiheAssembler.ToDataContract(ausleihe);

        return response;
    }
}

Der Businesscase Controller führt die Aktion auf der Aggregatwurzel aus.

public class BuecherAusleihenController
{
    public Ausleihe LeiheBuecherAus(string benutzerSid, Buch[] buecher)
    {
        if (buecher == null) { throw new ArgumentNullException(nameof(buecher)); }
        if (string.IsNullOrEmpty(benutzerSid)) { throw new ArgumentNullException(nameof(benutzerSid)); }

        Benutzerkonto benutzerkonto = benutzerkontoRepository.QueryBySidForUpdate(benutzerSid);

        Ausleihe ausleihe = benutzerkonto.LeiheAus(buecher);
        benutzerkontoRepository.Flush();

        InformiereUeberAusgelieheneBuecher(ausleihe);

        return ausleihe;
    }
}

Das Domänenobjekt führt dann die späte Validierung durch.

Stärken und Schwächen

Stärken
  • Die Integration der späten Validierung mit dem O/R Mapper vermindert die Gefahr von inkonsistenten Daten in der Datenbank
  • Die Validierung wird direkt auf dem Domänenmodell forciert
  • Es ist möglich, für spezielle Operationen eine frühe Validierung durchzuführen
  • Die Implementierung ist intuitiv und unkompliziert
Schwächen

keine

Benutzte Muster

Cookie Consent mit Real Cookie Banner