Thomas Bandt

Über mich | Kontakt | Archiv

Validierung gehört in den Business Layer

Ist doch selbstverständlich, oder? Scheint nicht so, zumindest nicht, wenn man den vielen Ansätzen von Microsoft folgt. Da gibt es seit ASP.NET 1.0 die Validation Controls für WebForms und mit ASP.NET MVC hielt eine Validierung über DataAnnotations Einzug. Letzteres ist auch verdammt sexy, da die Konfiguration über Attribute wirklich schön sauber ist. Deshalb habe ich es im ersten MVC-Projekt auch versucht zu verwenden.

Mir ist das Ganze aber ziemlich schnell auf die Füße gefallen. Von hausgemachten Problemen (Bools validieren, Passwort-Vergleich) einmal abgesehen, kommen dann auch schnell Szenarien, die einen ins Grübeln bringen.

Beispiel gefällig? Wie wäre es z.B. mit einer Benutzerregistrierung, bei der überprüft werden soll, ob ein Benutzername bereits vergeben wurde? Das geht nur mit komplexerer Logik, die einen Datenbankaufruf beinhaltet. So etwas möchte ich eigentlich nicht unbedingt im Frontend wissen und in ein Attribut packen.

Dazu kommt DRY. Denn was, wenn man nicht nur ein Web-, sondern auch ein Desktop-Frontend oder eine mobile Version erstellt? Dann muss die komplette Geschäftslogik, nichts anderes ist Validierung von Daten ja letztendlich auch, mehrfach implementiert werden.

Ich habe mich also, so schwer es mir fiel, von den hübschen DataAnnotations wieder verabschiedet und die Logik wieder dorthin gepackt, wohin sie gehört: in den Business Layer. Um mir die Arbeit zu erleichtern, gerade in Hinblick auf meinen Schwerpunkt ASP.NET MVC, habe ich mich am Beispiel "Validating with a Service Layer" orientiert. Mir gefallen aber keine magischen Strings, weshalb ich das Beispiel etwas abgewandelt habe.

   1:  public interface IValidationState
   2:  {
   3:      void AddError<T>(Expression<Func<T, object>> property, string errorMessage);
   4:      bool ContainsKey<T>(Expression<Func<T, object>> property);
   5:      bool IsValid { get; }
   6:  }

Außerdem sehe ich davon ab, es als Konstruktor-Parameter dem Service mitzugeben und verwende es stattdessen lieber auf Methodenebene (was den Kontext klarer abgrenzt).

Die Verwendung ist ziemlich simpel:

   1:  private void ValidateClientToCreate(Client client, IValidationState validationState)
   2:  {
   3:   
   4:      if(string.IsNullOrWhiteSpace(client.Name))
   5:          validationState.AddError<Client>(c => c.Name, "Name fehlt");
   6:   
   7:      if (string.IsNullOrWhiteSpace(client.Alias))
   8:          validationState.AddError<Client>(c => c.Alias, "Alias fehlt");
   9:   
  10:  }

Um es mit ASP.NET MVC nutzen zu können (geht auch mit 1.0), habe ich ebenfalls einen Wrapper geschrieben:

   1:  public class ViewModelValidationState : IValidationState
   2:  {
   3:   
   4:      public ViewModelValidationState(ModelStateDictionary modelState)
   5:      {
   6:          ModelState = modelState;
   7:      }
   8:   
   9:      private readonly ModelStateDictionary ModelState;
  10:   
  11:      public void AddError<T>(Expression<Func<T, object>> property, string errorMessage)
  12:      {
  13:          ModelState.AddModelError(GetKey(property), errorMessage);
  14:      }
  15:   
  16:      public bool ContainsKey<T>(Expression<Func<T, object>> property)
  17:      {
  18:          return ModelState.ContainsKey(GetKey(property));
  19:      }
  20:   
  21:      public bool IsValid
  22:      {
  23:          get { return ModelState.IsValid; }
  24:      }
  25:   
  26:      private static string GetKey<T>(Expression<Func<T, object>> property)
  27:      {
  28:          if (property.Body is UnaryExpression)
  29:              return ((UnaryExpression)property.Body).Operand.ToString().Split('.')[1];
  30:          return ((MemberExpression)property.Body).Member.Name;
  31:      }
  32:   
  33:  }

Nett an der Sache ist, dass man flexibel bleibt. So habe ich z.B. in einem Fall bei der Registrierung die Validierung der Kerndaten, die für jede Anwendung gleich ist, quasi an der Grenze zur Domäne, in den Service gepackt.

Aber natürlich kann es vorkommen, dass im Client selbst noch weitere Überprüfungen stattfinden müssen - z.B. die Prüfung, ob beim Formular das Passwort korrekt wiederholt wurde um Fehler zu vermeiden, oder ob die AGB akzeptiert worden sind. Das sind Dinge, die nicht in den Service gehören, weil sie in diesem Kontext schlicht nicht existent sind.

Also kombiniert man die Prüfungen einfach:

   1:  [HttpPost]
   2:  public ActionResult Index(SignUp model)
   3:  {
   4:   
   5:      if(!model.AgreesWithTerms)
   6:          ModelState.AddModelError("AgreesWithTerms", "AGB ...");
   7:   
   8:      if(model.UserPassword != model.UserPasswordConfirmation)
   9:          ModelState.AddModelError("UserPasswordConfirmation", "Passwort ...");
  10:   
  11:      var client = new Client();
  12:      var user = new User();
  13:   
  14:      if (ClientService.CreateClient(client, user, new ViewModelValidationState(ModelState)))
  15:      {
  16:          return RedirectToAction("success");
  17:      }
  18:   
  19:      return View(model);
  20:   
  21:  }

Ebenfalls gewrappt habe ich es natürlich für Tests:

   1:  public class MockValidationState : IValidationState
   2:  {
   3:   
   4:      private bool isValid = true;
   5:      private readonly Dictionary<string, string> errors = new Dictionary<string, string>();
   6:   
   7:      private static string GetPropertyName<T>(Expression<Func<T, object>> property)
   8:      {
   9:          if (property.Body is UnaryExpression)
  10:              return ((UnaryExpression)property.Body).Operand.ToString().Split('.')[1];
  11:          return ((MemberExpression)property.Body).Member.Name;
  12:      }
  13:   
  14:      public void AddError<T>(Expression<Func<T, object>> property, string errorMessage)
  15:      {
  16:          errors.Add(GetPropertyName(property), errorMessage);
  17:          isValid = false;
  18:      }
  19:   
  20:      public bool ContainsKey<T>(Expression<Func<T, object>> property)
  21:      {
  22:          return errors.ContainsKey(GetPropertyName(property));
  23:      }
  24:   
  25:      public bool IsValid
  26:      {
  27:          get { return isValid; }
  28:      }
  29:   
  30:  }

Verwendung:

   1:  [Test]
   2:  public void Darf_der_Name_nicht_Null_sein()
   3:  {
   4:      var validationState = new MockValidationState();
   5:      Sut.CreateClient(new Client { Name = null }, new User(), validationState);
   6:      Assert.IsTrue(validationState.ContainsKey<Client>(c => c.Name));
   7:  }

Ob dies der Weisheit letzter Schluss ist, mal sehen ;-). Aber es erscheint mir momentan allemal sinnvoller als die Validierung komplett im GUI abzuwickeln, dazu lässt es sich sehr bequem nutzen.

Kommentare

  1. Mariusz schrieb am Dienstag, 30. März 2010 10:56:00 Uhr:

    Ich denke, dass du mit dieser Variante ziemlich gut fahren wirst. Für die Validierung gibt es eben keine 100%ige Lösung. Hier muss man unterscheiden, ob die Validierung im Frontend oder/und im Business Layer Sinn macht.
    Als ich die DataAnnotations gesehen hab, war ich anfangs hellauf begeistert. Schnell machte es sich jedoch unbrauchbar, als ich die Validierung mit Datenbank-Rückfragen koppeln musste. Für reine Input-Validierung sicherlich eine super Sache, für weitere Validierungen leider unbrauchbar.
  2. Thomas schrieb am Dienstag, 30. März 2010 11:04:00 Uhr:

    Ja, da muss man dann eben wieder beginnen zu mixen und Logik an einen Ort zu packen, wo sie nicht hingehört.

    Andererseits wird man früher oder später nicht umhin kommen derartige Sachen auch ins GUI zu packen, allein um dem Nutzer eine bessere "Experience" zu bieten. Und ob ich dann den Request z.B. für eine Nutzernamenprüfung per jQuery vom Browser aus starte oder eben vom Attribut im ViewModel ist dann auch egal.

    Aber worauf es mir ankommt ist die Tatsache, dass die Grenze zum "Inneren der Anwendung", also zur Domäne gesichert und nicht mehr abhängig von der Implementierung des Frontends ist.

    Und da man es wie gesagt ja beliebig mit Frontend-Validierungs-Regelen kombinieren kann, sollte das so gut klappen.
  3. Joe schrieb am Mittwoch, 7. April 2010 19:08:00 Uhr:

    Klasse Artikel! Verwende das Prinzip jetzt ebenso und benutze DataAnnotations lediglich für Übersetzungen! :-)
  4. Thomas goes .NET schrieb am Freitag, 14. Mai 2010 14:45:00 Uhr:

    Als ich vorhin am Artikel über das Mocken des HttpContext schrieb, wollte ich eigentlich eine Demo-Anwendung dazu packen. Als ich mit ihr fertig war, fiel mir auf, dass ich damit leicht über das Ziel hinaus geschossen war. Denn in der App ist viel me ...
  5. Thomas goes .NET schrieb am Samstag, 15. Mai 2010 11:05:00 Uhr:

    Ich habe mich zwar schon vor einiger Zeit von der Verwendung der DataAnnotations in meinen ernsthaften Projekten wieder verabschiedet, doch habe ich just diese Woche erst wieder ein kleineres Projekt damit umgesetzt (man glaubt ja gar nicht wie effiz ...
  6. Thomas goes .NET schrieb am Mittwoch, 28. Juli 2010 12:34:00 Uhr:

    Disclaimer: Ich bin kein Ajax-Entwickler. Als das Thema vor 5 Jahren auf den Tisch kam, hatte ich schon einige Jahre mit XmlHttpRequest im Internet Explorer gearbeitet, aber mich eigentlich schwerpunktmäßig immer auf die Serverseite konzentriert. Das ...


« Zurück  |  Weiter »