Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

Hauptmenü
myCSharp.de
» Startseite
» Forum
» Suche
» Regeln
» Wie poste ich richtig?

Mitglieder
» Liste / Suche
» Wer ist online?

Ressourcen
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Microsoft Docs

Team
» Kontakt
» Cookies
» Spenden
» Datenschutz
» Impressum

  • »
  • Community
  • |
  • Diskussionsforum
[Artikel] Attribute zur Prüfung von Properties verwenden
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

Themenstarter:

[Artikel] Attribute zur Prüfung von Properties verwenden

beantworten | zitieren | melden

Hallo Community,

oft besteht der Bedarf, in den Settern von Properties, den übergebenen Wert (value) zu prüfen, bevor er in einem Feld gespeichert wird. Meist erfolgt dies durch händisches Abfragen und Werfen von Exceptions.


public String Street
{
   ...
   set {
      If (value.Length > 20) { // ≤ Implementierung einer Prüfung
         throw new Exception ("Zu lang");
      }
      _strStreet = value;
    }
}
Dies hat mehrere Nachteile. Aus Sicht des Implementierer der Property wird der Code - insbesondere wenn mehrere Prüfungen erforderlich sind - unnötig aufgebläht und gleichzeitig redundant. Aus Sicht des Benutzers der Property verschwinden die Prüfungen in der Implementierung und sind nach außen nicht bekannt.

Diese Nachteile lassen sich mit einem Schlag beheben, wenn man durch Attribute angibt, welche Prüfungen erfolgen (sollen), die Prüfungen also quasi nur noch deklariert.


[MaxStringLength (20)] // ≤ Deklaration einer Prüfung
public String Street
{
   ...
   set {
      AttributeChecker <String>.Check (value);
      _strStreet = value;
    }
}
Damit ist nach außen nicht nur automatisch dokumentiert, dass die maximale Stringlänge 20 ist, sondern ein Benutzer der Klasse kann diese Information sogar zu Laufzeit abfragen, z.B. um die Größe bzw. die maximale Länge einer TextBox entsprechend festzulegen.

Bei diesem Ansatz gab es zwei Herausforderungen zu bewältigen. (1) In der Attribut-Klasse gibt es keine direkte Unterstützung, generisch die Attribute der aktuellen Property zu erhalten. Auch für die umgekehrte Richtung gibt es keine direkte Unterstützung. (2) Ein Attribut-Objekt weiß also nicht, zu welcher Property es gehört. Dank der allgemeinen Reflection konnten aber beide Probleme gelöst werden.

Der folgende Code zeigt, wie das Ganze funktioniert. Um das Ausprobieren zu erleichtern, habe ich alle benötigten Klassen und Interfaces in eine Anwendung gepackt. Da die Interfaces und Hilfsklassen nur einmal vorhanden sein müssen und dann in allen (kommenden) Modellklassen benutzt und wiederverwendet werden können, würde man den Code in einer "echten" Anwendung natürlich in mehrere Dateien aufteilen.


using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

//*****************************************************************************
// Interface
public interface CheckableAttribute
{
}

//*****************************************************************************
// Interface
public interface CheckableAttribute <T> : CheckableAttribute
{
   //==========================================================================
   void Check (PropertyInfo pi, T tValue);
}

//*****************************************************************************
// Technische Hilfsklasse
public static class AttributeChecker <T>
{
   //==========================================================================
   public static void Check (T tValue)
   {
      Helper.SayHuhu ();

      MethodBase   mb;
      String       strProperty;
      Type         typDeclaring;
      PropertyInfo pi;

      //-----------------------------------------------------------------------
      // Ermitteln von welcher Property welcher Klasse wir aufgerufen wurden
      //-----------------------------------------------------------------------
      mb = new StackFrame (1).GetMethod ();

      if (!mb.IsSpecialName) {
         throw new Exception ("PropertyCheckException: "
                            + "Check wurde nicht direkt von einer "
                            + "Property aus aufgerufen");
      }

      strProperty = mb.Name.Substring (4);
      typDeclaring = mb.DeclaringType;
      pi = typDeclaring.GetProperty (strProperty,
                                     (mb.IsStatic
                                      ? BindingFlags.Static
                                      : BindingFlags.Instance)
                                   | (mb.IsPublic
                                      ? BindingFlags.Public
                                      : BindingFlags.NonPublic));

      if (pi == null) {
         throw new Exception ("PropertyCheckException: "
                            + "Check wurde nicht direkt von einer "
                            + "Property aus aufgerufen");
      }

      //-----------------------------------------------------------------------
      // (1) Ermitteln der (prüfbaren) Attribute zur der Property
      // Anm. Hier wird absichtlich das allgemeine CheckableAttribute
      //      verwendet
      //-----------------------------------------------------------------------
      Object [] achkattr = pi.GetCustomAttributes (
                              typeof (CheckableAttribute),
                              true
                           );

      //-----------------------------------------------------------------------
      // Eigentliche Prüfung
      // Anm. Hier wird absichtlich das spezifische CheckableAttribute <T>
      //      verwendet. Dadurch gibt eine Exception, wenn ein
      //      CheckableAttribute vom falschen Typ verwendet wird.
      //-----------------------------------------------------------------------
      foreach (CheckableAttribute <T> chkattr in achkattr) {
         //--------------------------------------------------------------------
         // Durchführen der eigentlichen Prüfung und dabei
         // (2) mitteilen, zu welcher Property das Attribut gehört
         //--------------------------------------------------------------------
         chkattr.Check (pi, tValue);
      }
   }
}

//*****************************************************************************
// Technische Hilfsklasse
public static class Helper
{
   //==========================================================================
   public static void SayHuhu ()
   {
      // besser Debug.WriteLine
      Console.WriteLine ("==> "
                       + new StackFrame (1).GetMethod ().DeclaringType.Name
                       + "."
                       + new StackFrame (1).GetMethod ().Name);
   }
}

//*****************************************************************************
// Hilfsklasse
[AttributeUsage (AttributeTargets.Property)]
public class MaxStringLengthAttribute : Attribute, CheckableAttribute <String>
{
   //--------------------------------------------------------------------------
   private int _iMaxStringLength;

   //==========================================================================
   public MaxStringLengthAttribute ()
   {
      Helper.SayHuhu ();
      _iMaxStringLength = -1;
   }

   //==========================================================================
   public MaxStringLengthAttribute (int iMaxStringLength)
   {
      Helper.SayHuhu ();
      _iMaxStringLength = iMaxStringLength;
   }

   //==========================================================================
   public void Check (PropertyInfo pi, String strValue)
   {
      Helper.SayHuhu ();

      //-----------------------------------------------------------------------
      // Nötigenfalls Ermitteln der Feldlänge
      //-----------------------------------------------------------------------
      if (_iMaxStringLength < 0) {
         // besser Debug.WriteLine
         Console.WriteLine ("Simulierter Datenbankzugriff, "
                          + "um die Feldlänge zu ermitteln");
         if (pi.Name == "Street") {
            // besser Debug.WriteLine
            Console.WriteLine ("Simulierter Datenbankzugriff erfolgreich");
            _iMaxStringLength = 20;
         }
      }

      //-----------------------------------------------------------------------
      // Eigentliche Prüfung
      //-----------------------------------------------------------------------
      if (strValue.Length > _iMaxStringLength) {
         throw new Exception ("MaxStringLengthException: "
                            + pi.DeclaringType.Name
                            + "."
                            + pi.Name
                            + @": """
                            + strValue
                            + @""" ist "
                            + strValue.Length
                            + " Zeichen lang. Erlaubt sind aber nur "
                            + _iMaxStringLength
                            + " Zeichen.");
      }
   }
}

//*****************************************************************************
// Hilfsklasse
[AttributeUsage (AttributeTargets.Property)]
public class StringFormatAttribute : Attribute, CheckableAttribute <String>
{
   //--------------------------------------------------------------------------
   private String _strPattern;

   //==========================================================================
   public StringFormatAttribute (String strPattern)
   {
      Helper.SayHuhu ();
      _strPattern = strPattern;
   }

   //==========================================================================
   public void Check (PropertyInfo pi, String strValue)
   {
      Helper.SayHuhu ();
      if (!Regex.IsMatch (strValue, "^" + _strPattern + "$")) {
         throw new Exception ("StringFormatException: "
                            + pi.DeclaringType.Name
                            + "."
                            + pi.Name
                            + @": """
                            + strValue
                            + @""" entsprich nicht dem Muster ""^"
                            + _strPattern
                            + @"$"".");
      }
   }
}

//*****************************************************************************
// Modellklasse
public class Address
{
   //--------------------------------------------------------------------------
   private String _strStreet;

   //==========================================================================
   public Address ()
   {
      Helper.SayHuhu ();
   }

   //==========================================================================
   [MaxStringLength ()]
   [StringFormat ("[a-zA-ZäöüÄÖÜß ]*")]
   public String Street
   {
      get {
         Helper.SayHuhu ();
         return _strStreet;
      }

      [MethodImpl (MethodImplOptions.NoInlining)]
      set {
         Helper.SayHuhu ();
         AttributeChecker <String>.Check (value);
         _strStreet = value;
       }
   }
}

//*****************************************************************************
// Hauptklasse
static class App
{
   //==========================================================================
   public static void Main (string [] astrArg)
   {
      Helper.SayHuhu ();

      //-----------------------------------------------------------------------
      Address addr = new Address ();

      //-----------------------------------------------------------------------
      Console.WriteLine ("");
      Console.WriteLine ("Setze korrekten Straßennamen");
      try {
         addr.Street = "Kurz genug";
      }
      catch (Exception exc) {
         Console.WriteLine (exc.Message);
      }

      //-----------------------------------------------------------------------
      Console.WriteLine ("");
      Console.WriteLine ("Setze zu langen Straßennamen");
      try {
         addr.Street = "Dieser Straßenname ist zu lang";

      }
      catch (Exception exc) {
         Console.WriteLine (exc.Message);
      }

      //-----------------------------------------------------------------------
      Console.WriteLine ("");
      Console.WriteLine ("Setze Straßennamen mit ungültigen Zeichen");
      try {
         addr.Street = "!õ$%&/(";
      }
      catch (Exception exc) {
         Console.WriteLine (exc.Message);
      }
   }
}
Um die Prüfungen bezüglich des Typs der Property (String, int, ...) generisch, aber gleichzeitig typsicher zu machen, habe ich Generics aus .NET 2.0 verwendet. Der Code ließe sich aber auch so umschreiben, dass er unter .NET 1.1 verwendbar wäre. Dazu müssten - grob gesprochen - alle Teile in spitzen Klammern entfernt und der Typ T durch Object ersetzt werden. In der Folge werden dann einige weitere Änderungen wie z.B. Casts fällig. Außerdem muss an drei Stellen static vor class entfernt werden.

Über Anmerkungen und Verbesserungsvorschläge würde ich mich freuen. [EDIT]Diese sind erfreulicherweise in großer Zahl erfolgt. Außerdem habe ich selber im Verlaufe des Threads einige Weiterentwicklungen vorgestellt. So finden sich weiter unten nicht nur Verbesserungen hinsichtlich der Ermittlung der aktuellen Property, sondern sogar eine Variante, die auf AOP/PostSharp basiert.[/EDIT]

herbivore

PS: gfoidl hat einige Ideen aus diesem Artikel aufgegriffen. Er hat ein eigenes Validierungssystem geschaffen und in ValidationRules und Attribute zur Validierung veröffentlicht. Wo meinen Ansatz auf dem Werfen von Exceptions basiert, setzt er auf das IDataErrorInfo-Interface, weshalb sich sein Ansatz viel besser für DataBinding-Szenarien eignet. Außerdem nutzt sein Ansatz zwar standardmäßig Attribute, um die Regeln anzugeben, ist aber nicht darauf beschränkt. Bei ihm können die Regeln von beliebigen Orten stammen, z.B. aus der Konfiguration oder aus einer Datenbank. Außerdem ist in seinem Ansatz die Lokalisierung der Fehlermeldungen bereits implementiert. Mit anderen Worten: Mein Artikel beschreibt ein Konzept, gfoidl bietet eine fertige Komponente.

PPS: Weiter unten werden weitere Möglichkeiten der Ermittlung des Namens der Property beschrieben, den man braucht, um auf die Attribute zuzugreifen. Hervorzuheben sind die Möglichkeit per MethodBase.GetCurrentMethod ().Name und die Möglichkeit per CallerMemberName (ab .NET 4.5 / C# 5.0).


Siehe auch
[Artikel] Attributbasierte Programmierung
ValidationRules und Attribute zur Validierung
private Nachricht | Beiträge des Benutzers
svenson
myCSharp.de - Member



Dabei seit:
Beiträge: 8775
Herkunft: Berlin

beantworten | zitieren | melden

Schöne Lösung. Was für CodeProjekt!

Hast du mal eine Performanceanalyse gemacht? Um welchen Faktor ist der Spass teurer?

Das wird dir übrigens in dem Zusammenhang sehr gefallen:

Including Assertions in .NET Assemblies

Leider akademisch. Gibt nur den Artikel.

Etwas anders aber in die Richtung:
http://www.resolvecorp.com/Products.aspx
private Nachricht | Beiträge des Benutzers
frisch
myCSharp.de - Member

Avatar #avatar-1724.gif


Dabei seit:
Beiträge: 2118
Herkunft: Coburg / Oberfranken

beantworten | zitieren | melden

Hallo herbivore,

wirklich gut was du da verzapft (^^) hast.


Großes Lob!
Es ist toll jemand zu sein, der nichts von der persönlichen Meinung Anderer hält. - frisch-live.de
private Nachricht | Beiträge des Benutzers
Xqgene
myCSharp.de - Member



Dabei seit:
Beiträge: 2189

beantworten | zitieren | melden

dem kann ich auch nix mehr hinzufügen.
"A programmer is a tool which converts coffein to code."

Evely ToDo-Manager 1.2 (Build 1.2.585)
private Nachricht | Beiträge des Benutzers
Fabian
myCSharp.de - Member

Avatar #avatar-1590.jpg


Dabei seit:
Beiträge: 1994
Herkunft: Dortmund

beantworten | zitieren | melden

Hallo herbivore,

sehr sehr schöne Lösung. Einfach nur gut gemacht! Eine Performanceanalyse, wie sie svenson schon vorgeschlagen hat, würde mich auch noch interessieren.


Gruß,
Fabian
"Eine wirklich gute Idee erkennt man daran, dass ihre Verwirklichung von vornherein ausgeschlossen erscheint." (Albert Einstein)

Gefangen im magischen Viereck zwischen studieren, schreiben, lehren und Ideen umsetzen…

Blog: www.fabiandeitelhoff.de
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Hallo zusammen,

erstmal vielen Dank für die vielen freundlichen Antworten.
Zitat
Hast du mal eine Performanceanalyse gemacht? Um welchen Faktor ist der Spass teurer?
Frag doch nicht sowas :-)

Die Performance ist natürlich grausam. Ich habe mal die beiden Varianten verglichen, die ich am Anfang meines Artikels gegenübergestellt habe (max. Länge 20). Alle Consolen-Ausgaben haben habe ich entfernt. Ich habe nur den Fall getestet, dass die Prüfung bestanden wird. Der Faktor ist 4000, was ich auch nicht besonders verwunderlich finde. Die Verwendung der Property ohne Attribute kostet ja nur den Methodenaufruf des Setters, eine Abfrage und eine Zuweisung, also quasi nichts. Bei Verwendung der Property mit Attributen werden etliche Objekte erzeugt und alleine jede einzelne Objekterzeugung dürfte um einiges teurer sein, als der gesamte Zugriff auf eine attributlose Property. Außerdem sind es nicht irgendwelche Objekte, die da erzeugt werden, sondern die teureren Reflection-Objekte.

Allerdings ist 4000 mal Nichts trotzdem nicht wirklich viel. Auf meinem Rechner kann ich pro Sekunde ca. zwanzigtausend Properties setzen. Das würde in einer Datenbankanwendung vermutlich nicht der Flaschenhals sein.

Ca. 2/3 der Performance gehen übrigens auf das Konto der Ermittlung von strProperty und typDeclaring und dabei wiederum macht GetStackFrame den Löwenanteil aus.

Leider bin ich bei der Performance-Analyse auf eine weitere Herausforderung gestoßen. Im Moment funktioniert mein Code nur im Debug-Modus zuverlässig, da im Release-Modus scheinbar die Aufrufe des Setters wegoptimiert werden (inline). Es gibt dann also keinen Stackframe für den Aufruf des Setters und entsprechend liefert GetStackframe (1) den Stackframe der Methode, die den Setter aufruft. Damit schlägt aber die Ermittlung von strProperty und typDeclaring fehl. Die Prüfung kann nicht durchgeführt werden. Leider werde ich mich vermutlich erst in zwei Wochen diesem Thema wieder intensiv widmen können.

herbivore
private Nachricht | Beiträge des Benutzers
der Marcel
myCSharp.de - Member

Avatar #avatar-1860.gif


Dabei seit:
Beiträge: 564
Herkunft: Dresden

beantworten | zitieren | melden

Hallo herbivore!



der Marcel
:] :DDer größte Fehler eines modernen Computers sitzt meist davor :]
private Nachricht | Beiträge des Benutzers
Fabian
myCSharp.de - Member

Avatar #avatar-1590.jpg


Dabei seit:
Beiträge: 1994
Herkunft: Dortmund

beantworten | zitieren | melden

Hallo herbivore,

Faktor 4000 ist natürlich schon etwas. Wobei, wie Du schon sagtest, wird das nicht der Flaschenhals sein.
Danke für Deine Analyse.

Die zweite, auch im Release-Modus funktionierende Variante, würde mich auch interessieren .


Gruß,
Fabian
"Eine wirklich gute Idee erkennt man daran, dass ihre Verwirklichung von vornherein ausgeschlossen erscheint." (Albert Einstein)

Gefangen im magischen Viereck zwischen studieren, schreiben, lehren und Ideen umsetzen…

Blog: www.fabiandeitelhoff.de
private Nachricht | Beiträge des Benutzers
Xqgene
myCSharp.de - Member



Dabei seit:
Beiträge: 2189

beantworten | zitieren | melden

Zitat
Original von Fabian
Die zweite, auch im Release-Modus funktionierende Variante, würde mich auch interessieren .

*g* mich auch.

als Abhilfe könnte man der Check-Methode den Namen der Property und Type des aktuellen Objektes mitteilen

      
set 
{
...
         AttributeChecker <String>.Check (value, this.GetType(), "PropertyName");
...
}

ist aber Copy&Paste -fehleranfällig. was aber beim Debugen schnell gefunden werden kann:


   public static void Check (T tValue, Type objType, string propName)
   {
      PropertyInfo pi;

#if DEBUG
      String       strProperty;
      Type         typDeclaring;
      MethodBase   mb;
      //-----------------------------------------------------------------------
      // Ermitteln von welcher Property welcher Klasse wir aufgerufen wurden
      //-----------------------------------------------------------------------
      mb = new StackFrame (1).GetMethod ();

      if (mb.DeclaringType != objType)
         throw new Exception("Der übergebene Typ des Objektes stimmt nicht mit" +
                            " dem Typ des prüfendes Objektes.");

      if (!mb.IsSpecialName) {
         throw new Exception ("PropertyCheckException: "
                            + "Check wurde nicht direkt von einer "
                            + "Property aus aufgerufen");
      }

      strProperty = mb.Name.Substring (4);
      
      if (strProperty != propName)
            throw new Exception("Es wurde falscher PropertyName übergeben");
#endif

      pi = objType.GetProperty (propName,
                                     (mb.IsStatic
                                      ? BindingFlags.Static
                                      : BindingFlags.Instance)
                                   | (mb.IsPublic
                                      ? BindingFlags.Public
                                      : BindingFlags.NonPublic));
.....
"A programmer is a tool which converts coffein to code."

Evely ToDo-Manager 1.2 (Build 1.2.585)
private Nachricht | Beiträge des Benutzers
VizOne
myCSharp.de - Member

Avatar #avatar-1563.gif


Dabei seit:
Beiträge: 1551

beantworten | zitieren | melden

Man könnte natürlich dem setter ein


[MethodImpl(MethodImplOptions.NoInlining)]

verpassen um das inlinen zu verhindern. Aber schöner wird es davon auch nicht

Grüße,
Andre
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Hallo zusammen,

das Feine an so einem Forum ist, dass man nicht alles selber machen muss. Die beiden Vorschläge von Xqgene und VizOne lösen das Problem. Jeder kann jetzt selbst überlegen, was er schöner findet.

Da es mein selbstgesetztes Ziel war, die redundante und "copy&paste-fehleranfällige" Angabe von Property-Namen und -Typen zu vermeiden, würde ich mich persönlich für die Variante von VizOne entscheiden.

Dass man ein zusätzliches Attribut braucht, stört mich nicht. Wenn man das von mir vorgeschlagene Verfahren anwendet, sollte man ohnehin ein Fan von Attributen sein. Dann kommt es aber auch auf ein Attribut mehr oder weniger nicht an.

Und dass die Inlining-Optimierung für den Setter ausgeschaltet ist, macht angesichts des Faktors 4000 den Kohl nun ins keinster Weise fett.

Sollte man das Attribut vergessen, bekommt man im Releasemodus beim Setzen der Property die Exception "Check wurde nicht direkt von einer Property aus aufgerufen". Wenn man das Attribut also nicht gerade bei einer äußerst selten gesetzen Property vergisst, merkt man es sofort beim ersten Echttest.

Ich sehe damit die Herausforderung als bewältigt an. Ich habe im Programm ganz oben das NoInlining-Attribut hinzugefügt. Damit funktioniert das Programm jetzt auch im Releasmodus zuverlässig.

Vielen Dank an VizOne und Xqgene. Bei Xqgene, Talla und svenson möchte ich mich bei dieser Gelegenheit für die Anregungen und Rückmeldungen im Vorfeld bedanken. Und nicht zuletzt geht mein Dank an Kostas, der mich überhaupt erst angestiftet hat, mich mit dem Thema zu beschäftigen.

herbivore
private Nachricht | Beiträge des Benutzers
talla
myCSharp.de - Experte

Avatar #avatar-3214.jpg


Dabei seit:
Beiträge: 7290
Herkunft: Esslingen

beantworten | zitieren | melden

Die Danksagung liest sich ja wie bei einem Buchtext Strebst so eine veröffentlichung mal an?
Baka wa shinanakya naoranai.

Mein XING Profil.
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Hallo talla,
Zitat
Die Danksagung liest sich ja wie bei einem Buchtext
für ein Buch müssten die Danksagungen noch viel länger werden. Mindestens eine halbe Seite. Sonst macht das nichts her. :-) Ansonsten Ehre wem Ehre gebührt.
Zitat
Strebst so eine veröffentlichung mal an?
Mit der Veröffentlichung auf mycsharp bin ich voll und ganz zufrieden. Vielleicht packe ich später mal einige meiner Beiträge von mycsharp überarbeitet und aktuallisiert auf meine (momentan leere) Homepage. Aber das ist Zukunftsmusik.

herbivore
private Nachricht | Beiträge des Benutzers
Fabian
myCSharp.de - Member

Avatar #avatar-1590.jpg


Dabei seit:
Beiträge: 1994
Herkunft: Dortmund

beantworten | zitieren | melden

Hallo zusammen,
Zitat
Original von herbivore
Mit der Veröffentlichung auf mycsharp bin ich voll und ganz zufrieden.

Ist das Thema nicht schon so "groß" und gut ausgearbeitet, dass man es auf CodeProject veröffentlichen könnte? Nur mal so als Anregung, da das Problem mit den vielen manuell geschriebenen Checks ja eigentlich sehr häufig auftritt.


Gruß,
Fabian
"Eine wirklich gute Idee erkennt man daran, dass ihre Verwirklichung von vornherein ausgeschlossen erscheint." (Albert Einstein)

Gefangen im magischen Viereck zwischen studieren, schreiben, lehren und Ideen umsetzen…

Blog: www.fabiandeitelhoff.de
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Hallo Fabian,

kann schon sein, dass der Artikel gut genug für Codeprojekt wäre. Und gerade wenn, finde ich es gut, dass es auf mycsharp (im doppelten Sinne) exklusiven Content gibt.

herbivore
private Nachricht | Beiträge des Benutzers
Fabian
myCSharp.de - Member

Avatar #avatar-1590.jpg


Dabei seit:
Beiträge: 1994
Herkunft: Dortmund

beantworten | zitieren | melden

Hallo herbivore,
Zitat
Original von herbivore
Und gerade wenn, finde ich es gut, dass es auf mycsharp (im doppelten Sinne) exklusiven Content gibt.

Stimmt, hab' ich gar nicht dran gedacht. Dadurch wird myCSHARP nur noch interessanter.


Gruß,
Fabian
"Eine wirklich gute Idee erkennt man daran, dass ihre Verwirklichung von vornherein ausgeschlossen erscheint." (Albert Einstein)

Gefangen im magischen Viereck zwischen studieren, schreiben, lehren und Ideen umsetzen…

Blog: www.fabiandeitelhoff.de
private Nachricht | Beiträge des Benutzers
FZelle
myCSharp.de - Experte



Dabei seit:
Beiträge: 10065

beantworten | zitieren | melden

Ist zwar jetzt schon eine ziemlich Zeit her, aber ich beschäftige mich derzeit
auch mit Reflection und DynamicMethod.

Was in dem Beispiel wirklich die meiste Zeit kostet ist das

 pi = typDeclaring.GetProperty
und das

achkattr = pi.GetCustomAttributes
Wenn Du das Cached geht es deutlich schneller.
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Hallo FZelle,

das deckt sich zumindest nicht ganz mit meiner Messung:
Zitat
Ca. 2/3 der Performance gehen übrigens auf das Konto der Ermittlung von strProperty und typDeclaring und dabei wiederum macht GetStackFrame den Löwenanteil aus.
Die eigentliche Lösung liegt m.E. darin den Aufwand auf die Compilezeit zu verlagern, wie das auch in Event auf Methoden-Aufruf über Attribute (natürlich auch davor und danach) diskuiert wird.

herbivore
private Nachricht | Beiträge des Benutzers
FZelle
myCSharp.de - Experte



Dabei seit:
Beiträge: 10065

beantworten | zitieren | melden

Diese Aufrufe finden in der Release Version aber garnicht statt.

Wenn Du also den Release Teil nimmst, ist meine Aussage schon stimmig.

Und natürlich kann man das alles auch zur Compilezeit machen, aber wozu
wenn dieser Ansatz ja eigentlich OK ist.
private Nachricht | Beiträge des Benutzers
Peter Bucher
myCSharp.de - Experte

Avatar #jVxXe7MDBPAimxdX3em3.jpg


Dabei seit:
Beiträge: 6141
Herkunft: Zentralschweiz

beantworten | zitieren | melden

Hallo Herbivore

Eine interessante Idee und eine tolle Lösung dazu.
Kiompliment, weiter so )


Gruss Peter
--
Microsoft MVP - Visual Developer ASP / ASP.NET, Switzerland 2007 - 2011

- https://peterbucher.ch/ - Meine persönliche Seite
- https://fpvspots.net/ - Spots für FPV Dronenflüge
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Hallo FZelle
Zitat
Diese Aufrufe finden in der Release Version aber garnicht statt.
mit GetStackFrame ist new StackFrame (1) in Check (T tValue) gemeint und dieser Aufruf findet natürlich auch im Release statt. Die Aufrufe von SayHuhu sind in die Messung natürlich nicht eingeflossen.
Zitat
Und natürlich kann man das alles auch zur Compilezeit machen, aber wozu wenn dieser Ansatz ja eigentlich OK ist.
Hauptsächlich aus Performance-Gründen, denn ich denke auch durch ein Caching hätte man wegen des new StackFrame immer noch mindestens einen Faktor vor 1000 gegenüber der eincompilierten Version. Außerdem finde ich es eine grundsätzlich eine interessante Idee, den Code zur Compilezeit (oder sagen wir besser zur Buildzeit) instrumentieren zu können; nicht nur für diesen Fall.


Hallo Peter Bucher,

vielem Dank für das Lob!

herbivore
private Nachricht | Beiträge des Benutzers
FZelle
myCSharp.de - Experte



Dabei seit:
Beiträge: 10065

beantworten | zitieren | melden

Sorry hatte beim "hochblättern" nur den Vorschlag von Xqgene gesehen,
und da ist das mit "#if debug" ausgeschaltet.

Und ich persönlich halte nichts von Exceptions in Settern, aber das eine andere Sache.
Schon mal IDataErrorInfo angeschaut?
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Hallo FZelle,

IDataErrorInfo kannte ich noch nicht. Aber meine Implementierung wäre ja leicht von Exceptions auf IDataErrorInfo anzupassen, wenn das dem eigene Stil entspräche. Mir ging es darum die Prüfung in eine deklarative Form zu bingen, nicht darum, wie die Prüfung das Ergebnis übermittelt.

herbivore
private Nachricht | Beiträge des Benutzers
FZelle
myCSharp.de - Experte



Dabei seit:
Beiträge: 10065

beantworten | zitieren | melden

Ja, ist ja richtig, aber die Exceptions bei Settern wiegen Leute immer in trügerische Sicherheit.

Diese Exceptions werden nämlich beim DataBinding meist geschluckt,
weshalb dann ein zustand entsteht, der subobtimal ist.

IDataErrorInfo ist sehr nett, weil ein gebundener Errorprovider dann
zeimlich einfach dem Benutzer seine Fehler mitteilt.
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Hallo zusammen,

ich habe mich heute nochmal der Performance angenommen und dazu den Code von ganz oben auf auf die Verwendung von PostSharp (http://www.postsharp.org/) umgestellt. Und ich kann schon verraten: es hat sich gelohnt!
Zitat
PostSharp, a .NET post-compiler: AOP and more

With PostSharp, you can develop custom attributes that change your code.
Na, das ist doch genau, was wir wollen. :-)
Zitat
And you can do more!

PostSharp is a post-compiler: an open platform for analysis and transformations of .NET assemblies after compilation.

Aspect-Oriented Programming (AOP) or Policy Injection are two of the major applications, but only two of them.
PostSharp Laos is a high-level aspect weaver that makes aspect-oriented programming (AOP) extremely easy. But Laos is only an illustration of the complete Platform. PostSharp is used to perform low-level MSIL injection. It serves as a base for persistence layers, optimizers or custom AOP weavers.
Wenn man Postsharp nutzen möchte, muss man es sich natürlich herunterladen und installieren.

Gut, aber zurück zur Umstellung des Codes. Diese war erstaunlich einfach. Die Struktur musste so gut wie gar nicht angepasst werden. Hier was wir uns alles sparen können:
  • Die Interfaces CheckableAttribute und CheckableAttribute <T> sowie die Klasse AttributeChecker <T>, die ja bisher das Weben implementiert hat, können alle ersatzlos entfallen.
  • Entsprechend entfällt auch der explizite Aufruf von AttributeChecker <String>.Check (value); das macht Postsharp implizit. Ein sehr angenehmer Nebeneffekt.
  • Entfallen kann auch das bisherige Attribut [MethodImpl (MethodImplOptions.NoInlining)], das ja nur nötig war, damit die AttributeChecker-Klasse beim Weben im Releasemode nicht durcheinander kam.
Zusätzlich brauchen wir nur das:
  • Einen neuen Namespace using PostSharp.Laos;
Und ein paar Sachen müssen noch geändert/angepasst werden:
  • Die Klassen MaxStringLengthAttribute und StringFormatAttribute müssen als [Serializable] gekennzeichnet werden.
  • Die bisherige Vererbungsliste dieser beiden Klassen (: Attribute, CheckableAttribute <String>) wird ersetzt durch die Postsharp-Oberklasse : OnMethodBoundaryAspect.
  • Die Methodenköpfe public void Check (PropertyInfo pi, String strValue) werden ersetzt durch die von Postsharp vorgegeben Methodenköpfe public override void OnEntry (MethodExecutionEventArgs e)
  • Alle weiteren Änderungen ergeben sind draus, dass die für die Überprüfung notwendigen Informationen in den MethodExecutionEventArgs stecken und nicht in den bisherigen Parametern PropertyInfo pi und String strValue. Es sind aber genau die gleichen Informationen (und sogar noch viel mehr) vorhanden, nur anders verpackt.
Das sind zwar eine ganze Reihe von Änderungen, aber im Prinzip eben alles nur Details. Die Struktur bleibt die gleiche, außer dass das explizite Weben durch AttributeChecker-Klasse durch den impliziten Mechanismus von Postsharp ersetzt wird.

So, nun aber zur Performance: Die Version mit Postsharp gegenüber der bisherigen Version um ca. Faktor 40 schneller! Ok, damit sind die Zugriffe zwar immer noch um Faktor 100 langsamer als eine direkte Prüfung, aber wenn man berücksichtigt, dass Postsharp ja für jede Prüfung ein Objekt vom Typ MethodExecutionEventArgs erstellen muss, ist das nicht verwunderlich. Aber nur noch Faktor 100 gegenüber dem bisherigen Faktor 4000 ist ein so großer Fortschritt, dass ich wirklich keine Bedenken hätte, den Mechanismus in realen Programm einzusetzen. Immerhin sind jetzt auf meinem Rechner ca. 800.000 Zugriffe auf Properties pro Sekunde möglich (gegen über den bisherigen 20.000).

Neben der reinen Code-Änderung, muss man Postsharp in den Build-Prozess einbinden, was jedoch leicht von der Hand geht:

Man muss eine Referenz auf PostSharp.Laos.dll und PostSharp.Core.dll zum Projekt hinzufügen und außerdem die erstellte EXE mit Postsharp nachbearbeiten. So wie ich das verstanden habe, geht diese Nachbearbeitung automatisch, wenn man die VS-Integration von Postsharp installiert hat. Ich habe die Nachbearbeitung mit folgender Kommandozeile vorgenommen, die ich der Lesbarkeit wegen auf mehrere Zeilen umgebrochen habe:
PostSharp.exe "C:\Programme\PostSharp 1.0\Default.psproj"
   attr.exe /p:thumbsup:utput=attr_ps.exe
   /p:IntermediateDirectory=c:\windows\temp /p:CleanIntermediate=true
   /p:DependenciesDirectory=.
Mit dieser Zeile wird aus attr.exe die erwünschte endgültige EXE-Datei attr_ps.exe. Man kann zwar auch attr.exe ausführen, aber bei dieser werden die Attribute beim Setzen von Properties nicht berücksichtigt. Das passiert erst in attr_ps.exe.

Hier der vollständige Code der Postsharp-Variante:


using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using PostSharp.Laos; // PS

//*****************************************************************************
// Technische Hilfsklasse
public static class Helper
{
   //==========================================================================
   public static void SayHuhu ()
   {
      // besser Debug.WriteLine
      Console.WriteLine ("==> "
                       + new StackFrame (1).GetMethod ().DeclaringType.Name
                       + "."
                       + new StackFrame (1).GetMethod ().Name);
   }
}

//*****************************************************************************
// Hilfsklasse
[Serializable]
[AttributeUsage (AttributeTargets.Property)]
public class MaxStringLengthAttribute : OnMethodBoundaryAspect
{
   //--------------------------------------------------------------------------
   private int _iMaxStringLength;

   //==========================================================================
   public MaxStringLengthAttribute ()
   {
      Helper.SayHuhu ();
      _iMaxStringLength = -1;
   }

   //==========================================================================
   public MaxStringLengthAttribute (int iMaxStringLength)
   {
      Helper.SayHuhu ();
      _iMaxStringLength = iMaxStringLength;
   }


   //==========================================================================
   public override void OnEntry (MethodExecutionEventArgs e)
   {
      Helper.SayHuhu ();

      //-----------------------------------------------------------------------
      // Argumente holen
      //-----------------------------------------------------------------------
      Object [] aobjArgs = e.GetArguments ();
      if (aobjArgs == null) {
         Console.WriteLine ("getter called");
         return;
      }
      String strValue = (String)aobjArgs [0];

      //-----------------------------------------------------------------------
      // Nötigenfalls Ermitteln der Feldlänge
      //-----------------------------------------------------------------------
      if (_iMaxStringLength < 0) {
         // besser Debug.WriteLine
         Console.WriteLine ("Simulierter Datenbankzugriff, "
                          + "um die Feldlänge zu ermitteln");
         if (e.Method.Name.Substring (4) == "Street") {
            // besser Debug.WriteLine
            Console.WriteLine ("Simulierter Datenbankzugriff erfolgreich");
            _iMaxStringLength = 20;
         }
      }

      //-----------------------------------------------------------------------
      // Eigentliche Prüfung
      //-----------------------------------------------------------------------
      if (strValue.Length > _iMaxStringLength) {
         throw new Exception ("MaxStringLengthException: "
                            + e.Method.DeclaringType.Name
                            + "."
                            + e.Method.Name.Substring (4)
                            + @": """
                            + strValue
                            + @""" ist "
                            + strValue.Length
                            + " Zeichen lang. Erlaubt sind aber nur "
                            + _iMaxStringLength
                            + " Zeichen.");
      }
   }
}

//*****************************************************************************
// Hilfsklasse
[Serializable]
[AttributeUsage (AttributeTargets.Property)]
public class StringFormatAttribute : OnMethodBoundaryAspect
{
   //--------------------------------------------------------------------------
   private String _strPattern;

   //==========================================================================
   public StringFormatAttribute (String strPattern)
   {
      Helper.SayHuhu ();
      _strPattern = strPattern;
   }

   //==========================================================================
   public override void OnEntry (MethodExecutionEventArgs e)
   {
      Helper.SayHuhu ();

      //-----------------------------------------------------------------------
      // Argumente holen
      //-----------------------------------------------------------------------
      Object [] aobjArgs = e.GetArguments ();
      if (aobjArgs == null) {
         Console.WriteLine ("getter called");
         return;
      }
      String strValue = (String)aobjArgs [0];

      //-----------------------------------------------------------------------
      // Eigentliche Prüfung
      //-----------------------------------------------------------------------
      if (!Regex.IsMatch (strValue, "^" + _strPattern + "$")) {
         throw new Exception ("StringFormatException: "
                            + e.Method.DeclaringType.Name
                            + "."
                            + e.Method.Name.Substring (4)
                            + @": """
                            + strValue
                            + @""" entsprich nicht dem Muster ""^"
                            + _strPattern
                            + @"$"".");
      }
   }
}

//*****************************************************************************
// Modellklasse
public class Address
{
   //--------------------------------------------------------------------------
   private String _strStreet;

   //==========================================================================
   public Address ()
   {
      Helper.SayHuhu ();
   }

   //==========================================================================
   [MaxStringLength ()]
   [StringFormat ("[a-zA-ZäöüÄÖÜß ]*")]
   public String Street
   {
      get {
         Helper.SayHuhu ();
         return _strStreet;
      }

      set {
         Helper.SayHuhu ();
         _strStreet = value;
       }
   }
}

//*****************************************************************************
// Hauptklasse
static class App
{
   //==========================================================================
   public static void Main (string [] astrArg)
   {
      Helper.SayHuhu ();

      //-----------------------------------------------------------------------
      Address addr = new Address ();

      //-----------------------------------------------------------------------
      Console.WriteLine ("");
      Console.WriteLine ("Setze korrekten Straßennamen");
      try {
         addr.Street = "Kurz genug";
      }
      catch (Exception exc) {
         Console.WriteLine (exc.Message);
      }

      //-----------------------------------------------------------------------
      Console.WriteLine ("");
      Console.WriteLine ("Setze zu langen Straßennamen");
      try {
         addr.Street = "Dieser Straßenname ist zu lang";

      }
      catch (Exception exc) {
         Console.WriteLine (exc.Message);
      }

      //-----------------------------------------------------------------------
      Console.WriteLine ("");
      Console.WriteLine ("Setze Straßennamen mit ungültigen Zeichen");
      try {
         addr.Street = "!õ$%&/(";
      }
      catch (Exception exc) {
         Console.WriteLine (exc.Message);
      }
   }
}
herbivore
private Nachricht | Beiträge des Benutzers
Kabelsalat
myCSharp.de - Member

Avatar #avatar-1937.jpg


Dabei seit:
Beiträge: 371
Herkunft: Bodensee

beantworten | zitieren | melden

Ist bekannt, ob Microsoft vorhat deklarative Parameterüberprüfung mit Visual Studio "Orcas" / .Net 3.5 einzuführen? Zeigt Microsoft Ambitionen sich dem Thema AOP (Aspect Oriented Programming) anzunehmen?

Hat sich von euch schonmal jemand mit Phx.Morph auseinandergesetzt (http://www.columbia.edu/~me133/ , http://www.codeplex.com/phoenixs1)?
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von Kabelsalat am .
private Nachricht | Beiträge des Benutzers
dr4g0n76
myCSharp.de - Experte

Avatar #avatar-1768.jpg


Dabei seit:
Beiträge: 3047
Herkunft: Deutschland

beantworten | zitieren | melden

@Kabelsalat: Nein, die zeigen da keine Ambitionen bisher, da so etwas ähnliches in der EnterpriseLib schon existiert (Policy-Injection und ähnliches)

Auch der Versuch Enhancer Technologien nutzbar zu machen im 3.x .NET Framework bleibt bisher aus. Ebenso unterstützt dies auch die Sprach-Spezifikation C# 3.0 nicht.

Bleib bisher nur auf PostSharp und ähnliche oder selbst implementierte Compile-Techniken auszuweichen.

Das Problem ist ja nur, was Herbivore auch beschreibt, dass nur über Stackframe und den Aufruf der Helperklasse möglich ist, die Attribute auch aufzurufen, während man sich z.B. in den Properties befindet. PostSharp injiziert diesen Code, um die Attribute gleich, wenn sie benötigt werden, aufzurufen.
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von dr4g0n76 am .
Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.
private Nachricht | Beiträge des Benutzers
ikaros
myCSharp.de - Member



Dabei seit:
Beiträge: 1787

beantworten | zitieren | melden

Zitat
kann schon sein, dass der Artikel gut genug für Codeprojekt wäre. Und gerade wenn, finde ich es gut, dass es auf mycsharp (im doppelten Sinne) exklusiven Content gibt.
Dem schliesse ich mich gerne an. Nur den doppelten Sinn versteh ich gerade nicht(mir genügt ein einfacher).

Das Performanceproblem(Faktor 4000 halte ich für übertrieben) liegt m.E. am Test-Modell. Attribute sind eine schöne Sache, nur muss nicht jedesmal eine neue Instanz nachgeladen werden um diese auch noch zu reflektieren.

Zitat


public String Street
{
   ...
   set {
      If (value.Length > 20) { // ≤ Implementierung einer Prüfung
         throw new Exception ("Zu lang");
      }
      _strStreet = value;
    }
}
Dies hat mehrere Nachteile. Aus Sicht des Implementierer der Property wird der Code - insbesondere wenn mehrere Prüfungen erforderlich sind - unnötig aufgebläht und gleichzeitig redundant. Aus Sicht des Benutzers der Property verschwinden die Prüfungen in der Implementierung und sind nach außen nicht bekannt.
Mal abgesehen vom DescriptionAttribut, was hindert jemanden beim Setter auf eine andere Klasse zuzugreifen?

Ich halte dein Beispiel für sehr gut, in Sachen dass Attribute näher gebracht werden. Ob Regeln als Aspekt gehandhabt werden sollten, ist eher philosophisch. Im praktischen Sinn, ist es per Attribut oft zu langsam. (Faktor 4000 ist aber understatement(nicht nachvollziehbar)).
Halte ich es ganz einfach: verschliesst sich der Sinn der Trennung im Beispiel. Es existiert eine Property, dafür muss aber man eine eigene Klasse schreiben...
Existierte sowas wie Dynamik(oder würde erklärt) zur Laufzeit o.ä. würden mehr den Nutzen erkennen.

Fazit: ich find's gut weil der Nutzen von Attributen gut erklärt wird. Andere die Lösung selbst. Aber ich fürchte die meisten haben es nicht verstanden.
Technich ok, aber etwas unverständlich für Anfänger bzw. Fortgeschrittene Programmierer denen Design einfach abgeht.
private Nachricht | Beiträge des Benutzers
dr4g0n76
myCSharp.de - Experte

Avatar #avatar-1768.jpg


Dabei seit:
Beiträge: 3047
Herkunft: Deutschland

beantworten | zitieren | melden

@ikaros: Ich finde es nicht unverständlich.
Ausserdem sind Attribute meiner Meinung nach eine gute Möglichkeit deklarative Programmierung zu benutzen (also auch im Sinne von AOP aber nicht nur), wenn es Richtung Enhancer geht, unterstützt Microsoft dies immer noch nicht. Auch im neuesten Framework bzw. Compiler ist nichts dazu vorhanden, wenn man mal die Enterprise-Library ausser acht läßt, die ja auch nur quasi Runtime-Proxies bietet.
Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.
private Nachricht | Beiträge des Benutzers
ikaros
myCSharp.de - Member



Dabei seit:
Beiträge: 1787

beantworten | zitieren | melden

@dr4g0n76:
Deiner Meinung zu Attributen schliesse ich mich gern an. Wie schon erwähnt finde ich die angewandte Technik auch nicht schlecht. Im Gegenteil. Ich hoffte mich da verständlich ausgedrückt zu haben.
Herbivore hat gute Arbeit geliefert. Nur finde ich das das Beispiel nicht ganz passt. Für die geschilderte Problemstellung finde ich die Lösung(trotz der Klasse) als 2.e Wahl. Grund ist: das Problem selbst lässt sich trivialer lösen(einmal unschön), besser: anders(fast attributslos). Bezogen auf das Beispiel lassen sich halt zuviele Alternativen finden, die sich auch in kürzerer Codeform wiederspiegeln lassen.
Das ist der Punkt den ich etwas kritisiere. Die angewandte Technik erschliesst sich daurch nicht unbedingt dem Neuling(dieser propagierten Technik). Das liegt aussschliesslich am Beispiel.

Übrigens, was mich ein wenig nervt: Ist die quasi nicht vorhandene Trennung zwischen Artikel und Diskussion.
Ich hab das Gefühl das zumindest in diesen Artikelthread der Artikel unter der Diskussion leidet.
private Nachricht | Beiträge des Benutzers