User Tools

Site Tools


patterns:inheritancerolestragegyoverview:strategy

Das Strategie Muster

Strategien sind mächtige und sinnvolle Werkzeuge für Verhalten, das austauschbar ist. Zudem kann das Verhalten von allen abgeleiteten Klassen geerbt werden1). Ein tolles Beispiel hierfür ist die Authentifizierung eines Kontos. Es gibt diverse Möglichkeiten2) wie ein Konto authentifiziert werden kann. Beispiele sind:

  • über die Angabe eines Benutzernamens und eines Kennwortes,
  • über eine vorhandene Authentifizierung, z.B. wenn ein Anwender bereits an Windows angemeldet ist,
  • über ein X.509 Zertifikat,
  • über einen Dongle,
  • gar nicht,
  • usw.

Wenn die Authentifizierung als Strategie umgesetzt wird, kann man sich erst mal für eine entscheiden. Die anderen können dann bei Bedarf dazu gebaut werden, und in einigen Fällen werden neue Strategien dazu kommen, die zum jetzigen Zeitpunkt noch gar nicht bekannt waren.

Strategien werden vom Client3) an die Domäne übergeben. Das ist ein so immens wichtiger Punkt, dass ich ihn nicht genug betonen kann. Ich hatte mir Gedanken gemacht, wie…

  • man die Strategie als Teil des Aggregates definiert,
  • und dass das Aggregate ein Ereignis veröffentlicht wie z.B. new AuthenticationStrategySet([hier den AssemblyQualifiedName der Strategie]),
  • dann müsste man pro Strategie eigene Befehle definieren,
  • der CommandHandler dafür müsste das Aggregate aufrufen, die Strategie daraus abfragen, und dann die entsprechende Methode in der Strategie aufrufen,
  • Die Strategie müsste dann ein Ereignis erzeugen, dass der AggregateRoot mitgeteilt wird, da das Repository beim Speichern die AggregateRoot nach den ungespeicherten Ereignissen fragt,
  • wird ein Aggregate neu geladen, müsste über das Ereignis AuthenticationStrategySet die entsprechende4) Strategie geladen werden5), am besten über eine Factory die irgendwie in das Aggregate eingespiesen wird,
  • dabei kann man nicht einfach über die Typenbezeichnung den Acitvator verwenden, um die Strategie zu erzeugen, da sie vielleicht weitere Konstruktoreigenschaften benötigt6),
  • dann müssen die Ereignisse die Strategie betreffend von der Aggregate Root an die Strategie weiter geleitet werden, da das AggregateRoot sie gar nicht kennt und nicht weiß, was damit anzufangen ist. Dafür müssten sich die Strategie Ereignisse alle ableiten von z.B. AccountAuthorizationStrategyEvent, und die Apply-Methode im AggregateRoot für diesen Basistyp würde wissen, es an die Strategie zu übergeben.

Das scheint erst mal ziemlich cool, Aggregates dynamisch durch Strategien erweitern zu können. Im Sinne von DDD7) ist es aber einfacher, wenn die Strategien übergeben werden. Wie das gehen soll, kann am besten am Beispiel der Authentifizierung aufgezeigt werden.

Strategie Muster in der Authentifizierung

Die AuthenticationStrategy ist abstrakt. Sie kann beliebig implementiert werden, z.B. von einer KerberosAuthenticationStrategy. Diese hat eigene Befehle und eigene Ereignisse. Wie in dem Beispiel des Benutzerkontos kann man einen AuthenticationStrategyInfo Kontext definieren, zu dem sich die einzelnen Implementierungen der Strategie verpflichten, ihn mit Leben zu füllen8).

Der springende Punkt, der eine Strategie ausmacht

  • Die Klasse AuthenticationStrategy hat eine Methode Authenticate(Account account),
  • Account hat eine Methode Authenticate(AuthenticationStrategy withStrategy). Das ist die Methode, in der die Strategie an das Konto übergeben wird. Sie wird wie folgt aufgerufen9)
public void Execute(AuthenticateAccount message, SecurityPrincipal principal)
{
    // Die Repositories werden im Konstruktor des Handlers gesetzt. Der AccountHandler kann dafür auch ein Repository<AuthenticationStrategy> haben, schadet nicht
    var account = _accntRepository.GetById(message.AccountId, message.RequiredVersion);
    var strategy = _authenticationStrategyRepository.GetById(message.AuthenticationStrategyId, message.RequiredVersion);
 
    account.AuthenticateWith(strategy); // ****** HIER WIRD DIE STRATEGIE DAFÜR VERWENDET, WOZU SIE ÜBERHAUPT GEBAUT WURDE ******
 
    _authenticationStrategyRepository.Save(strategy); // nur Änderungen in der Strategie. Ansonsten müsste ein Domain Service daraus gemacht werden.
}

In Account sieht der Aufruf von AuthenticateWith aus wie folgt

public void AuthenticateWith(AuthenticationStrategy strategy)
{
    if (!strategy.AccountId.Equals(Id)) throw StrategyConfigurationException(); // muss nicht, kann aber. Wenn man die StrategyId im Account referenziert und anders herum ist eine 1-zu-1 Beziehung hergestellt. Nicht wie in einer Datenbank, aber wenn wie hier darauf geprüft wird ist die Beziehung Bomben sicher
    if (_isInactive) throw new AccountInactiveException(); // wozu dann überhaupt prüfen
    strategy.Authenticate(this);
}

Viel passiert hier nicht, d.h. die Authentifizierung direkt über die AuthenticationStrategy reicht vielleicht aus. Wozu der Account an die AuthenticationStrategie in Authenticate übergeben wird wird hier nicht verdeutlicht. Im Sinne von tell, don't ask kann Account hier z.B. übergeben ob das Konto aktiv ist, die eigene ID, damit die Strategie die Relation auch nochmal prüfen kann, usw. D.h. Account und AuthenticationStrategie fragen sich nicht gegenseitig nach ihren Eigenschaften um eigene Entscheidungen zu treffen, es werden Befehle gesendet.

Erstellen eines Kontos mit einer Authentifizierungsstrategie

Es gibt zwei Möglichkeiten. Entweder sendet man die Befehle zur Erstellung des Kontos und der Strategie gemeinsam an einen transaktionalen Command Service10). Dann weiß man, dass ein Konto immer mit einer Strategie erzeugt wird.

Die Alternative ist die, dass man immer erst die Strategie speichert, danach das Konto11). Wenn auch nur das Konto die Strategie referenziert und nicht anders herum, dann ist sicher gestellt, dass diese Referenz auch vorhanden ist wenn das Konto gespeichert wird12).

Es kann bei dieser Alternative nur passieren, dass die Strategie gespeichert wurde, das Konto aber nicht. In dem Fall hat man eine verwaiste Strategie ohne Konto. Damit der angegebene Benutzername in der Strategie nicht als verwendet gilt, sollten die verwendeten Benutzernamen einer Strategie über eine Sicht geprüft werden, die ein Konto mit einer Strategie verknüpft. Dafür wird ein Event Handler auf AccountAdded und KerberosAuthenticationStrategyAdded Ereignisse lauschen, und diese Sicht entsprechend aufbauen. Solange AccountAdded nicht erhalten wurde, gilt die Strategie als nicht-existent und blockiert den Usernamen nicht.

Blöd nur, wenn der Event Handler mal eine halbe Stunde nicht am Start ist. Dann würde der Anwender das Konto mit dem Benutzernamen immer wieder anlegen. Die Befehle würden erfolgreich durch laufen, weil nichts gegen die Neuanlage spricht, das Konto für “Ramin Tavassoli” mit dem Benutzernamen “rtavassoli” wird in der Sicht ja nicht gefunden, also ist alles gut. Sobald der Handler wieder am Start ist, gibt es zu “Ramin Tavassoli” plötzlich 8 Konten mit “rtavassoli” als Benutzernamen.

Die Konsequenzen wären nicht schlimm. Mit “rtavassoli” könnte sich nicht angemeldet werden, weil es den Benutzernamen mehr als einmal gibt, und daher nicht klar ist, welche KerberosAuthenticationStrategy verwendet werden soll. Also muss man sieben der Acht Konten wieder löschen.

In diesem Fall wäre das Spielchen vielleicht besser umgedreht. Erst das Konto speichern, dann die Strategie. Wenn die Strategie nicht angelegt werden kann, zeigt die AuthenticationStrategyId in Account ins Leere. Dann wird halt eine neue Strategie angelegt, und vorher die AuthenticationStrategyId im Account darauf geändert. Das kann solange laufen, bis es geklappt hat. Zumindest werden in den 8 Versuchen, in denen das Login “rtavassoli” neu angelegt wird, die alten immer abgelöst, am Ende gibt es also wirklich nur ein Konto mit “rtavassoli”.

Das Problem hierbei wäre nur, dass man erlauben muss, die Authentifizierungsstrategie im Konto ändern zu können. Das wollte ich eigentlich nicht erlauben, aber andererseits schadet es ja auch nicht.

Ich halte es aber generell für äußerst bedenklich, dass es die Möglichkeit gibt, dass Befehle erfolgreich durch laufen, mit der Möglichkeit, dass man das Ergebnis nicht gleich sieht. Was, wenn ich die Person “Ramin Tavassoli” anlege, das Ergebnis aber nicht sehe? Dann lege ich sie wieder an. Immer noch nicht da? Also nochmal anlegen. Am Ende gibt es “Ramin Tavassol” drei mal. Wie löse ich das? Oder besser noch, wie verhindere ich das?

Synchrone Deserialisierer

  1. Solange das funktioniert, werden Ereignisse synchron13) denormalisiert,
  2. Befehle, die zusammen gehören14) werden gemeinsam geschickt, um in einer gemeinsamen Transaktion gespeichert zu werden,
  3. Der Dienst, der Befehle abarbeitet, startet zu Beginn eine Transaktion und beendet sie am Ende der Aktion. Dort kann ein sinnvolles Timeout gesetzt werden15), damit Befehle nicht minutenlang16) unsichtbar weiter machen, und plötzlich Daten erscheinen, mit denen gar nicht mehr gerechnet wurde,
  4. Alle Sichten, Listen, Reports, usw. auf die synchron denormalisierten Sichten bauen. Nur Daten asynchron denormalisieren, wenn es absolut notwendig ist, und wenn der einzelne Fall wasserdicht gelöst ist17),
  5. Asynchrone Denormalisierer fragen regelmäßig die Datenlieferanten nach Ereignissen ab. Die Lieferanten liefern neue Ereignisse zusammen mit ihrer eigene Aktualität der Daten18). Die Denormalisierer schreiben zu jeder Sicht die Aktualität19). Wenn eine Sicht abgefragt wird, wird immer die Datenaktualität mit abgefragt20).

Wann werde ich schon Domänen partitionieren müssen? Daher sollte ich mit der Lösung, alles synchron zu halten21), im Grunde sehr weit kommen. Ich komme ansonsten mit Eventual Consistency absolut nicht klar, kann einfach nicht akzeptieren, dass der Anwender das versteht, und vor allem nicht, dass Daten potenziell Stunden oder Tage alt sein können, trotzdem Entscheidungen getroffen werden und Aktionen durchgeführt werden. Wenn sogar ständig Entwickler in den Foren ihre Fragen und Antworten reposten, weil sie nicht sofort zu sehen sind, wie soll man das dann von einem nicht technisch versierten Anwender verlangen können?

So würde eine vollständige Kerberos Authentifizierung aussehen

In diesem Fall muss der Client wissen, welche Strategie er zur Authentifizierung verwenden möchte, d.h. er muss explizit eine Kerberos Authentifizierung starten. Die Kerberos Authentifizierung läuft in mehreren Schritten ab. Das sollte aber wie folgt möglich sein:

  1. über den Benutzernamen22) fragt der Client nach der AuthenticationStrategyID und der AccountID. Dafür wird die Sicht verwendet, die ein Konto und die Authentifizierungsstrategie gemeinsam denormalisiert. Wenn die Sicht zu dem Benutzernamen mehr als eine Antwort findet, oder keine, wird ein entsprechender Fehler ausgelöst.
  2. der Client sendet nun eine Anfrage mit der AuthenticationStrategyID nach dem verschlüsselten AuthenticationKey und dem AuthenticationTicket an die Query-Seite des Kontos23),
  3. Der Kerberos Dienst geht hier direkt zum Repository24), lädt die AuthenticationStrategie, wandelt sie in eine KerberosAuthenticationStrategy, und fragt sie nach den angeforderten Daten25),
  4. Der Client entschlüsselt mit dem Kennwort den AuthenticationKey, verschlüsselt damit die ID und das Timeout, das mit dem letzten Aufruf zurück erhalten wurde, und sendet diese beiden zusammen mit dem AuthenticationTicket26) als KerberosCredentials an das Konto als Authenticate Befehl,
  5. Hinweis27): Der AuthenticateCredentials Befehl ist abstrakt. Das ist für Befehle für Strategien generell sinnvoll. Es wird dann einen Command Handler geben, der AuthenticateKerberosCredentials Befehle behandelt. Dieser wird dann das Konto über das Repository laden, und eine entsprechende Methode zur Authentifizierung aufrufen. Dieser Methode können neben den Credentials dann weitere Dinge übergeben werden, wie z.B. die ICryptTickets Schnittstelle,
  6. Der Command Handler aus dem vorherigen Punkt lädt also das Konto, und ruft die entsprechende Authenticate Methode der Authenticate Strategie auf28). Wenn das klappt, wird ein Authenticated Ereignis veröffentlicht, das einen SessionKey enthält, verschlüsselt mit dem AuthenticationKey, und ein neues SessionTicket29),
  7. Wenn das nicht klappt, wird NotAuthenticated Ereignis ausgelöst. Nach 5 Versuchen wird das Konto gesperrt,
  8. Der Client pollt mitlerweile nach dem Ergebnis der Anmeldung. Erhält er ein positives Ergebnis, ist er angemeldet. Er verschlüsselt dann die UserId und das Timeout mit dem erhaltenen SessionKey, den er vorher mit dem AuthenticationKey entschlüsseln muss, und sendet dieses gemeinsam mit dem SessionTicket in jedem Aufruf an den Server mit,
  9. Der Server braucht jetzt das Konto nicht mehr zu laden um den Dienstaufruf zu authentifizieren. Anhand der mit gesendeten Credentials wird über Service Locator ein IAuthenticateCredentials Dienst geladen. Der Dienst für die Kerberos Credentials kann das von ICryptTickets verschlüsselte Ticket entschlüsseln30) und mit dem darin enthaltenen SessionKey die vom Client mit dem SessionKey verschlüsselten Daten ermitteln, und die UserId und das Timeout in beiden vergleichen. Stimmt alles, dann kennt der Client den SessionKey, was er ja nur kann, wenn er ihn mit dem AuthenticationKey entschlüsseln konnte, den er vorher mit dem Passwort31) entschlüsseln konnte.

Alles alt und ersetzt!!!

Eine Strategie wird vorbereitend in ein Aggregate eingebaut32). Sie wird abstrakt definiert. Sie kann beim Erzeugen des Aggregates gesetzt werden und kann Implementationsabhängig danach verändert werden oder nicht. Wie das Ganze in CQRS/ES funktioniert, kann am besten an einem Beispiel erläutert werden.

Die Benutzernamen Strategie zur Authentifizierung

Konstruktor des Aggregates mit Strategien

Da Strategien austauschbar und erweiterbar sind, muss das Aggregate wissen, wie es aus den gespeicherten Daten eine Strategie laden kann. Dafür gibt es eine ISelectAuthenticationStrategy Schnittstelle

public interface ISelectAuthenticationStrategy
{
    AuthenticationStrategy Load(AuthenticationStrategyDTO);
}

Im Konstruktor des Aggregates wird dieses Interface übergeben

public abstract Account: Aggregate
{
    private ISelectAuthenticationStrategy AuthStrategySelector;
    public Account(ISelectAuthenticationStrategy authStrategySelector) { AuthStrategySelector = authStrategySelector; }
    public Account(Guid id, ISelectAuthenticationStrategy authStrategySelector)
    {
        AuthStrategySelector = authStrategySelector;
        ...
    }
}

Das Aggregate33) wird entweder vom Command Handler neu erzeugt, oder vom Repository wenn es erneut geladen wird. Der Command Handler wird ein neuen UserAccount z.B. wie folgt erzeugen34)

public void Execute(AddUserAccount message, SecurityPrincipal principal)
{
    var authSelector = ContextRegistry.GetContext().GetObject("AccountAuthenticationStrategySelector") as ISelectAuthenticationStrategy;
    var authStrategyConverter = ContextRegistry.GetContext().GetObject("AccountAuthenticationStrategyConverter") as IConvertAuthenticationStrategy;
    var UserAccount = new UserAccount(message.AggregateId, message.PersonId, authStrategyConverter.convert(message.AuthenticationStrategyDTO, authSelector));
    _repository.Save(UserAccount);

IConvertAuthenticationStrategy kann dieselbe Implementierung wie ISelectAuthenticationStrategy verwenden, falls das AuthenticationStrategyDTO dieselbe Klasse ist wie das AuthenticationStrategyDTO das im Domain Event zum Speichern des Ereignisses verwendet wird35).

Wenn das Konto nicht vom Command Handler neu erstellt wird, sondern vom Repository erneut geladen wird, muss die Schnittstelle ISelectAuthenticationStrategy ebenfalls an das Aggregate übergeben werden. Das Repository kann beim Erzeugen eines Accounts nun ebenfalls über Service Locator das korrekte ISelectAuthenticationStrategy Interface instanziieren und damit den UserAccount leer erzeugen, bevor die Ereignisse angewendet werden. Die Implementierung von ISelectAuthenticationStrategy wird in aller Regel einen ServiceLocator verwenden, dazu gleich mehr.

Es wird die abstrakte Klasse AuthenticationStrategyDTO geben36), mit bestimmten Implementierungen37)

[DataContract]
public abstract AuthenticationStrategyDTO { }
 
[DataContract]
public UsernameAuthenticationStrategyDTO: AuthenticationStrategyDTO
{
    [DataMember]
    public string UserName { get; private set; }
    [DataMember]
    public string Password { get; private set; }
    public UsernameAuthenticationStrategyDTO(string userName, string password)
    {
        UserName = userName;
        Password = password;
    }
}

Ebenso wird es die tatsächlichen Strategien geben38)

public abstract AuthenticationStrategy { }
 
public UsernameAuthenticationStrategy: AuthenticationStrategy
{
    public Account AggregateRoot { get; set; }
    public string UserName { get; private set; }
    public string Password { get; private set; }
    public UsernameAuthenticationStrategy(string userName, string password)
    {
        UserName = userName;
        Password = password;
    }
 
    public UsernameAuthenticationStrategyDTO Serialize()
    {
        return new UsernameAuthenticationStrategyDTO(UserName, Password);
    }
 
    // hier die tatsächliche Funktionalität
}

Wenn der Konstruktor39) von UserAccount über den CommandHandler aufgerufen wird, wird nach erfolgreichen Prüfungen u.a. das Setzen der Strategie vorgenommen

public abstract UserAccount: Account
{
    public UserAccount(ISelectAuthenticationStrategy authStrategySelector)
        : base(authStrategySelector) { }
    public UserAccount(Guid id, AuthenticationStrategy authStrategy, ISelectAuthenticationStrategy authStrategySelector)
        : base(authStrategySelector)
    {
        // Prüfungen
        ApplyChange(new AuthenticationStrategySet(authStrategy.Serialize()); // es wird also das DTO in das Ereignis gepackt
    }
}

Das Ereignis AuthenticationStrategySet muss nun angewendet werden. Das passiert in der Basisklasse Account

public class Account: AggregateRoot
{
    private AuthenticationStrategy authenticationStrategy; // man braucht in der Domäne ja nur Eigenschaften die die Business Logik betreffen. In diesem Fall muss das Konto Befehle an die Strategie weiterleiten, d.h. sie muss zwingend gehalten werden
    ....
    private void Apply(AuthenticationStrategySet e)
    {
        authenticationStrategy = ISelectAuthenticationStrategy.Load(e.AuthenticationStrategyDTO);
        authenticationStrategy.AggregateRoot = this; // wichtig für das Auslösen von Ereignisse aus der Strategie heraus
    }
}

Da Apply auch vom Repository aufgerufen wird wenn das Konto erneut geladen wird, haben wir jetzt einen relativ einfachen Weg gefunden, um Strategien zu setzen und das Ergebnis zu speichern. Zudem kann der Strategie in ISelectAuthenticationStrategy.Load() alles Mögliche übergeben werden, was dem Konto unbekannt ist. Im Falle einer Kerberos Strategie z.B. eine Verschlüsselungsschnittstelle für die Tickets. Die Veschlüsselung kann zwar Teil der Kerberos Strategie sein, da das Konto aber nach erfolgreicher Ticket Erstellung nicht erneut verwendet wird solange das Ticket gültig ist, müssen sich die Strategie und der Ticket Authentifizierungsdienst eine Verschlüsselung teilen. Diese soll also nicht fest in der Domäne verankert sein, sondern austauschbar an die Domäne und an den Dienst übergeben werden.

Strategien werden in der Literatur immer sehr einfach Beispielhaft dargestellt. Sie haben alle dieselben Methoden mit denselben40) Parametern, dafür aber unterschiedliche Ergebnisse. Zudem haben sie keinen eigenen Zustand. Dazu einige Punkte, die im einzelnen besprochen werden

Zustand einer Strategie

Eine Strategie kann zur Durchführung ihrer Aufgabe weitere Informationen benötigen. Eine Strategie zur Authentifizierung per Benutzername und Kennwort muss wissen, gegen welches Kennwort authentifiziert werden muss. Sie muss also u.a. eine Methode haben, um das Kennwort zu ändern. Eine andere Strategie, z.B. für die Windows Authentifizierung, hält kein Kennwort für das Konto.

Um das umsetzen zu können, muss eine Strategie für die eigenen Methoden eigene Command Handler haben. Diese sind genauso umgesetzt wie die Command Handler des Aggregates, nur dass sie Methoden in der Strategie aufrufen und nicht im Aggregate. Dafür muss das Konto die Strategie freigeben41). Es heißt zwar, dass jede Änderung durch das Aggregate Root laufen müssen, wenn ich aber eine Methode auf einer Entität aufrufe, die mir vom Aggregate Root gegeben wurde, dann geschieht die Änderung rein logisch auch durch das Aggregate Root.

Der Command Handler für ChangeUserAccountPassword würde dann wie folgt aussehen

public class AccountUsernameAuthorizationHandler: RequestHandler, Executes<ChangeUserAccountPassword>
{
    private readonly Repository<Account> _repository;
    public AccountUsernameAuthorizationHandler(Repository<Account> repository)
    {
        _repository = repository;
    }
 
    public void Execute(ChangeUserAccountPassword message, SecurityPrincipal principal)
    {
        var accnt = _repository.GetById(message.AggregateId, message.RequiredVersion);
        (accnt.AuthenticationStrategy as UsernameAuthenticationStrategy).ChangePassword(message.Password);
        _repository.Save(accnt);
    }
}
Zwischenbemerkungen

Das fängt alles danach auszusehen, als ob UsernameAuthenticationStrategy vielleicht doch eher ein eigenes Aggregate wäre. Man gibt dem Account ein ISelectAuthenticationStrategy mit, was im Grunde ein Repository für eine Strategie ist; die Strategie hat eigene Befehle und Handler für diese; und wie wir sehen werden, auch eigene Ereignisse. Könnte man nicht einfach eine Strategie als eigenes Aggregate definieren und dann im Konto eine Strategie setzen indem die ID der Strategie dort gesetzt wird?

Die Antwort ist ja, könnte man. Die Id der Strategie verhält sich dann wie ein Funktionszeiger, was eine Strategie ja im Grunde ist. Es wäre sicher gestellt, dass das Konto immer genau eine Strategie hat. Man müsste noch sicherstellen, dass keine zwei Konten ein und dieselbe Strategie verwenden42).

Getrennte Aggregate wären in diesem Fall aber irgendwie merkwürdig, unnatürlich. Bei einer Rolle ist das nicht so sehr der Fall. Die Rolle Mitarbeiter würde auch ohne eine Person Sinn haben, und alles was man mit dem Mitarbeiter macht passiert unabhängig von der Person43). Man kann den Mitarbeiter eine Personalnummer geben, einem Projekt zuordnen, ein Arbeitszeitmodell geben, usw.

Eine UsernameAuthenticationStrategy ohne dazugehöriges Konto kann es aber nicht geben. Wenn ich mich gegen die Strategie mit meinem Benutzernamen und Kennwort authentifiziere, und es kein Konto dazu gibt, oder das Konto die Strategie gar nicht (mehr) verwendet, habe ich ins Leere authentifiziert. Zudem kann das Konto inaktiv sein, was Teil der Authentifizierung ist. D.h. dass das Verhalten der Strategie das Konto benötigt, bzw. verwendet.

Dann fällt mir der Fall ein, in dem man die Strategie für ein Konto ändert, z.B. von Username zu Windows. Bei einem anderen Konto macht man das anders herum. Die Ereignisse müssten so behandelt werden, dass die ersetzten Strategien abgeschaltet werden, ansonsten blockiert die Username Strategie des ersten Kontos eventuell einen Benutzernamen, der nun vom zweiten Konto verwendet werden soll.

Zusammengefasst, ist das alles machbar. Man kann die Strategie als eigenes Aggregate umsetzen, was vielleicht einiges vereinfacht. Andererseits ist es ein interessantes Gedankenspiel, das Strategie Muster in CQRS mit ES einzubauen, weil es vielleicht irgendwann Situationen gibt, in denen es nicht anders geht. Also weiter mit dem Beispiel.

Erzeugen und Veröffentlichen von Strategie Ereignissen

Wenn die Methode ChangePassword in der UsernameAuthenticationStrategy eines Kontos aufgerufen wird, wird die Methode nach erfolgreichen Prüfungen ein UsernameAuthenticationStrategyPasswordChanged Ereignis erstellen. Das Ereignis gehört zu dem Konto, es muss also die Id des Kontos beinhalten, und der AggregateRoot44) bekannt gemacht werden, damit es in die Liste der Ereignisse mit aufgenommen werden kann. Das passiert wie oben aufgezeigt in Apply(AuthenticationStrategySet e) von Account. Nun muss die Strategie die Ereignisse nur noch über die gesetzte AggregateRoot veröffentlichen. Dazu ist die Methode ApplyChange(DomainEvent @event) des AggregateRoots öffentlich45).

public class UsernameAuthenticationStrategy: AuthenticationStrategy
{
    ....
    public void ChangePassword(string password)
    {
        AggregateRoot.ApplyChange(new UsernamePasswordChanged(password));
    }
}

Anwenden von Strategie Ereignissen im Konto

Damit ist es fast getan. ApplyChange im Konto wird per Reflektion über die Parametertypen gefunden und aufgerufen. Was soll das Konto aber mit UsernamePasswordChanged anfangen? Es wird dafür keine entsprechende Apply Methode geben, weil das Konto ja gar nichts von allen möglichen Strategie Ereignissen weiß. Die Lösung ist die, dass alle Strategie Ereignisse von dem abstrakten AccountAuthenticationStrategyEvent abgeleitet werden.

[DataContract]
public abstract class AccountAuthenticationStrategyEvent: DomainEvent { }
 
[DataContract]
public class UsernamePasswordChanged : AccountAuthenticationStrategyEvent
{
    public UsernamePasswordChanged (string password) { Password = password; }
 
    [DataMember]
    public string Password{ get; private set; }
}

In Account werden dann alle AccountAuthenticationStrategyEvents reflektiv an die vorhandene Strategie angewandt.

public class Account: AggregateRoot
{
    ....
 
    private void Apply(AccountAuthenticationStrategyEvent e)
    {
        Apply(authenticationStrategy, e); // die authenticationStrategy muss es geben, und sie sollte das Ereignis auch bearbeiten können, weil sie es ja ausgelöst hat!
    }
    ...
}

Die Apply-Methode, die ein Objekt und ein Ereignis als Parameter hat, wendet das Ereignis reflektiv im Objekt an, genau wie das im AggregateRoot gemacht wird. So können Ereignisse mehrere Ebenen durchlaufen, sollte es mehrere Ebenen von erweiterbaren Entitäten im Aggregate geben.

Aufruf der Strategie Methode und öffentliche Methoden der Basisklasse

Letztendlich ist eine Strategie dafür da, dass ein Methodenaufruf in einem Objekt von der aktuell gesetzten Strategie übernommen wird. In dem Fall der Authentifizierung ist auch der Parameter des Aufrufs Strategie abhängig. Es wird also eine abstrakte Klasse Credentials geben, die dann von diversen Berechtigungsnachweisen46) abgeleitet werden, können, z.B. von Kerberos Tickets und Keys, von gehashten Passwörtern, usw. Der Client weiß ja, oder sollte wissen, wie er sich authentifizieren kann. Wenn er also Username-Credentials an ein Konto sendet, dessen Strategie eine Windows Authentifizierung ist, wird die WindowsAuthenticationStrategy mit den UsernameCredentials nichts anfangen können und einen Fehler auslösen. Naja, das ist ja so gedacht, oder47)?

Im Account wird es somit folgende Methode geben:

public void Authenticate(Credentials credentials)
{
    authenticationStrategy.Authenticate(credentials);
}

Das Konto delegiert den Aufruf an die aktuell gesetzte Strategie. Zudem kann das Konto Methoden für die Strategie über die gesetzte Schnittstelle freigeben48). Z.B. sollte die Strategie Account.Lock() aufrufen können, wenn z.B. 5 mal das falsche Kennwort angegeben wurde49). Die Strategie könnte auch Account.IsActive abfragen dürfen.

Alternative
public void Authenticate(Credentials credentials)
{
    if (_locked) throw new AccountLockedException(); // wozu dann noch die Strategie abfragen
    authenticationStrategy.Authenticate(credentials);
    if (authenticationStrategy.Authenticated) // ganz im sinne von CQS erst den Befehl, dann die Abfrage.
    {
        _securityPrincipal = authenticationStrategy.SecurityPrincipal; // der aufrufer/command handler speichert das aggregate. er kann es aber nun auch nach dem zustand abfragen: wieder CQS, erst befehl, dann abfrage. so kann der command handler sofort eine rückantwort an die gui geben, ob das login ein erfolg war oder nicht, ohne vorher die ereignisse abfangen zu müssen.
        _authenticated = authenticationStrategy.Authenticated;
    }
}

Das war's! Zugegeben, dadurch, dass man das Aggregate auf diese Art und Weise öffnet, öffnet man die Möglichkeit, dass die Strategien Ereignisse des Aggregates anwenden, die verboten sind. Das kann dadurch verhindert werden, dass IApplyChanges.ApplyChange im AggregateRoot, bzw. in der Entität50) verhindert, dass Ereignisse angewandt werden, die das AggregateRoot selbst auslöst. In dem Fall müssten die Entitäten durch Methoden des Aggregates gehen. Also alles in allem sehr sicher, stärker gekoppelt als getrennte Aggregates, und dennoch einfach anzuwenden, sobald das Framework dafür gebaut ist.

Alte Diskussion zu dem Thema, die noch aufgeräumt werden muss

  • Der Befehl, um ein Konto zu erzeugen, ist AddAccount(Guid id, AuthenticationStrategyDTO strategy). Dieser Befehl ist abstrakt, und von AddUserAccount implementiert werden. AddUserAccount würde wie folgt erzeugt werden:
    new AddUserAccount(new Guid("3F2504E0-4F89-11D3-9A0C-0305E82C3301"), PersonId, new UserNameStrategyDTO("abc", "Kennwort789"));
  • Der Command Handler ruft im Konto51) den Konstruktor wie folgt auf52)
    var accnt = new UserAccount(cmd.Id, cmd.PersonId, cmd.strategy);
  • Im Konstruktor von UserAccount wird nach allen Prüfungen das Ereignis UserAccountAdded(Guid id, Guid personId, AuthenticationStrategyDTO strategy) veröffentlicht über
    ApplyEvent(new UserAccountAdded(Id, PersonId, strategy));
  • Dabei kann53) das Kennwort kein einfacher string sein, sondern ein cryptedString54), der verschlüsselt in den Befehl und in das Domänenereignis geschrieben wird,
  • In dem UserAccount wird in Apply(UserAccountAdded @event) aus @event.strategy eine AuthenticationStrategy, ganz einfach über
    new AuthenticationStrategy(@event.UserName, Decrypt(@event.Password));

    also kein DTO das55) in Befehle und Ereignisse geschrieben wird, sondern ein Strategie Objekt mit einem entsprechenden Verhalten.

Was die Projektions-Seite angeht, läuft das ähnlich wie bei der Vererbung. Wenn man für die AuthenticationStrategy gemeinsame Eigenschaften identifizieren kann, kann man diese über einen weiteren Info56)-Kontext implementieren, und für jede Implementierung der Authentifizierungs Strategie müsste dieser Kontext befüllt werden.

Die Strategie verwenden

Wie kann eine Authentifizierungs Strategie verwendet werden? Authentifiziert werden immer die credentials57). Es muss also die abstrakte Klasse Credentials geben. Das UI erzeugt die entsprechenden Credentials, und sendet dann einen AuthenticateAccountCredentials Befehl an die Domäne58), z.B.

authenticationProxy.Apply(new AuthenticateAccountCredentials(accountId, textBoxPassword.Text));

Der Command Handler macht dann folgendes

public void Handle(AuthenticateAccountCredentials command)
{
    var repo = new Repository<Account>();
    var agg = repo.Get(command.Id);
    agg.Authenticator.Authenticate(command.Credentials);
    repo.Save(agg);    
}

In diesem Fall ist somit nicht nur die Strategie austauschbar, sondern auch die Information, die jede Strategie zur Prüfung benötigt. Möglicherweise sind die Anmeldeprozesse so unterschiedlich59), dass der Command Handler abhängig des Typs der Credentials noch über Service Locator einen weiteren Dienst aufruft, der weiß, wie die Authentifizierung im Einzelnen ablaufen muss.

Kerberos Authentifizierung

Die Kerberos Authentifizierung läuft in mehreren Schritten ab. Das sollte aber wie folgt möglich sein:

  1. über den Benutzernamen fragt das Front-End nach der Account-ID,
  2. ein Lesedienst fragt die denormalisierten Kerberos Konten ab60) und sendet das Ergebnis zurück an den Client,
  3. der Client sendet eine Anfrage mit der ID nach dem verschlüsselten AuthenticationKey und dem AuthenticationTicket an die Query-Seite des Kontos61),
  4. Der Kerberos Query Dienst lädt das Konto, wandelt die Strategie in eine KerberosAuthenticationStrategy, und fragt sie nach den angeforderten Daten62),
  5. Der Client entschlüsselt mit dem Kennwort den AuthenticationKey, verschlüsselt damit die ID und das Timeout, das mit dem letzten Aufruf zurück erhalten wurde, und sendet diese beiden zusammen mit dem AuthenticationTicket63) als KerberosCredentials an das Konto als Authenticate Befehl,
  6. Hinweis64): Der AuthenticateCredentials Befehl ist abstrakt. Das ist für Befehle für Strategien generell sinnvoll. Es wird dann einen Command Handler geben, der AuthenticateKerberosCredentials Befehle behandelt. Dieser wird dann das Konto über das Repository laden, und eine entsprechende Methode zur Authentifizierung aufrufen. Dieser Methode können neben den Credentials dann weitere Dinge übergeben werden, wie z.B. die ICryptTickets Schnittstelle,
  7. Der Command Handler aus dem vorherigen Punkt lädt also das Konto, und ruft die entsprechende Authenticate Methode der Authenticate Strategie auf65). Wenn das klappt, wird ein Authenticated Ereignis veröffentlicht, das einen SessionKey enthält, verschlüsselt mit dem AuthenticationKey, und ein neues SessionTicket66),
  8. Wenn das nicht klappt, wird NotAuthenticated Ereignis ausgelöst. Nach 5 Versuchen wird das Konto gesperrt,
  9. Der Client pollt mitlerweile nach dem Ergebnis der Anmeldung. Erhält er ein positives Ergebnis, ist er angemeldet. Er verschlüsselt dann die UserId und das Timeout mit dem erhaltenen SessionKey, den er vorher mit dem AuthenticationKey entschlüsseln muss, und sendet dieses gemeinsam mit dem SessionTicket in jedem Aufruf an den Server mit,
  10. Der Server braucht jetzt das Konto nicht mehr zu laden um den Dienstaufruf zu authentifizieren. Anhand der mit gesendeten Credentials wird über Service Locator ein IAuthenticateCredentials Dienst geladen. Der Dienst für die Kerberos Credentials kann das von ICryptTickets verschlüsselte Ticket entschlüsseln67) und mit dem darin enthaltenen SessionKey die vom Client mit dem SessionKey verschlüsselten Daten ermitteln, und die UserId und das Timeout in beiden vergleichen. Stimmt alles, dann kennt der Client den SessionKey, was er ja nur kann, wenn er ihn mit dem AuthenticationKey entschlüsseln konnte, den er vorher mit dem Passwort68) entschlüsseln konnte.
1) soll heißen, eine KerberosAuthenticationStrategy für ein Konto kann von UserAccount und von SystemAccount verwendet werden. Zudem sind die Strategien austauschbar
2) Strategien
3) dem Verwender der Domäne, also i.d.R. der Command Handler
4) erweiterbare
5) im Grunde ein Repository im Aggregate
6) deshalb eine Factory - was wirklich aufwendig ist
7) siehe Kapitel 12 im “Blue Book”
8) über entsprechende Event Handler, die die Info in den Kontext rein denormalisieren
9) nachdem die ganzen ID-Werte vorher ermittelt werden, dazu kommen wir noch. Das könnte auch ein Domain Service sein, wenn nötig, d.h. wenn das Konto und die Authentifizierung Änderungen schreiben müssen
10) was immer gehen wird, solange wir die Daten transaktional speichern können und die Aggregates auf einem gemeinsamen Rechner leben - was bei so eng zusammen hängenden Aggregaten der Fall sein wird; die werden niemals auf unterschiedliche Rechner aufgeteilt
11) das sollte generell so gemacht werden, dass immer erst das Objekt, das referenziert wird, gespeichert wird, dann das Objekt, das die Referenz hält - ähnlich wie in einer Datenbank
12) der mögliche Fall, dass mehrere Konten dieselbe Strategie referenzieren, wird beim Authentifizieren abgefangen. Das kann aber praktisch nicht passieren, nicht in 1.000.000 Jahren. Und wenn doch, wird es ja, wie gesagt, später entdeckt und immer abgefangen, bevor es zu echten Problemen führen kann
13) innerhalb derselben Transaktion, in der das Ereignis geschrieben wird
14) z.B. einen Kontext samt Strategie, oder eine Akteur samt Rolle
15) z.B. 10 Sekunden
16) oder theoretisch tagelang
17) d.h. Mechanismen bauen, die dazu führen, dass dem Anwender z.B. klar ist, dass er alte Daten sieht, und dass das auch nicht weiter schlimm ist
18) der Lieferant kann selbst Daten über Lieferanten erfragen, d.h. die Datenaktualität hangelt sich so durch
19) Als Uhrzeit
20) als Zeitraum, z.B. 1.00:02:23. Die Uhrzeit muss so nicht synchronisiert werden. Sie sollte lediglich auf dem Server nicht verstellt werden
21) außer Reports, bei denen der Anwender auch dahin erzogen werden kann, auf die Aktualität zu achten
22) z.B. “rtavassoli”
23) , 61) hierfür muss ein Kerberos Query Dienst eingerichtet sein
24) KerberosAuthenticationStrategy wird nicht mit CQRS, sondern mit CQS umgesetzt, kann also auch Fragen beantworten
25) , 28) , 62) , 65) wobei eine ICryptTickets Schnittstelle in dem Methdenaufruf mitgegeben wird
26) , 63) das vom Client nicht entschlüsselt werden kann
27) , 64) noch sauberer in den Workflow einbauen, ist mir gerade eingefallen
29) , 66) die Tickets sind durch ICryptTickes verschlüsselt, so dass es auch nicht vom Client entschlüsselt werden kann. Sie enthalten u.a. die UserId, das Timeout und den Key. Mit dem Key werden die vom Client verschlüsselten UserId und Session-Timeout Daten entschlüsselt, um zu prüfen, ob der Client das alles mit dem korrekten Schlüssel verschlüsselt hat - den Schlüssel kann der Client ja nur haben, wenn er initial das Passwort zur entschüsselung kannte.
30) , 67) Er ist i.d.R. der Dienst, der als ICryptTickets an das Konto übergeben wurde
31) , 68) mit dem MD5Hash vom Passort
32) Oder in eine neue Rolle zu dem Aggregate, falls der Bereich komplett neu dazu gebaut wird
33) in diesem Fall Account
34) das _repository wird per Constructor Injection gesetzt
35) muss es aber nicht
36) hier wird angenommen, dass diese Klasse für die Befehle verwendet wird, sowie für die Ereignisse. Das muss aber nicht der Fall sein
37) die über die Konfigurationsdatei den KnownTypes der AuthenticationStrategyDTO hinzugefügt werden
38) die DTOs sind für den Datenaustausch und die Datenhaltund des Zustands des Accounts da
39) oder eine Methode, die die Strategie ändert
40) meist keinen
41) was in DDD erlaubt ist - man darf Entitäten temporär raus geben
42) wenn man es umdreht, und die Kontonummer in die Strategie schreibt, müsste man sicherstellen, dass es zu einem Konto nicht mehrere Strategien gibt. Und eine Strategie als Rolle des Kontos mit einer geteilten Id des Kontos zu sehen geht nicht, wenn man die Strategie austauschbar haben möchte
43) zumindest auf der Befehlsseite
44) also dem Konto
45) Im Grunde sollte hier IApplyChange anstatt der AggregateRoot verwendet werden. So kann geschachtelt werden, und wenn E1.1.1 in AR–>E1–>E1.1–>E1.1.1 ein Ereignis über IApplyChange auslöst, können E.1.1 und E1 noch ihre internen ID Werte in dem Ereignis setzen, bevor die AR ID letzten Endes noch gesetzt wird
46) Credentials ;-)
47) Meldung an Anwender: “Anmeldung nicht möglich… usw.”
48) Oben wird in der Strategie das AggregateRoot gesetzt, das kann und sollte besser eine Schnittstelle sein, die von IApplyChanges abgeleitet ist
49) man könnte für das automatische Sperren des Kontos eine separate Strategie implementieren, das wäre aber eventuell zuviel des Guten
50) bei einer Verschachtelung
51) UserAccount
52) cmd ist der AddUserAccount Befehl
53) sollte
54) eigener Typ
55) teil-verschlüsselt
56) oder Lese
57) Berechtigungsnachweis
58) nachdem das Konto ermittelt wurde, das authentifiziert werden soll, z.B. über den Benutzernamen
59) z.B. Kerberos mit mehreren Nachrichten, die hin-und-her gesendet werden
60) SELECT ID FROM KerberosAccount WHERE UserName=@UserName
patterns/inheritancerolestragegyoverview/strategy.txt · Last modified: 2013/01/10 21:01 by rtavassoli