CCD: Domain POCO Adapter für fluent Filter

Ich brauche mal eure Hilfe, sitze hier alleine an meinem Schreibtisch und überlege, ob das alles so richtig ist. Leider gibt es niemanden mit dem ich das diskutieren könnte – ausser euch da draussen im Internet. Also, lasst es Kommentare regnen. :-)

Ein POCO “Family” hat typische Properties. Darunter auch Typen die nicht nullable sind (warum das wichtig ist, kommt noch). Um das Domänenmodell nicht zu verletzen, will ich das auch nicht ändern. Zum Beispiel gibt es das Property “ID” vom Typ GUID. Das ist ein typischer Primärschlüssel, der logischerweise nicht nullable ist und nicht sein darf.

jetzt kommt die andere Seite: Es gibt für das Family-POCO eine Methode GetFamily, die intern fluent filter einsetzt. Diese Methode wurde für maximale Flexibilität ausgelegt, was die Suchkriterien angeht.

Was wäre besser, EINE Methode mit 7 Parametern, von denen alle null sein dürfen? CCD lehrt uns daß man es vermeiden sollte, Methoden mit mehr als 3 Parametern zu schreiben.
Die Lösung ist ein Parameter-Container. Kann man dafür nicht gleich das Family-POCO nehmen? Nein, denn beliebige der Suchparameter dürfen null sein, so z.B. auch die ID. Das bietet Family aber nicht! Darum habe ich einen FamilyFilterAdapter geschrieben. Das Ding ist im Prinzip das Gleiche wie Family, nur daß alle Typen nullable sind.

   1: public class Family : IFamilyBase, IComparable
   2: {
   3:    public Guid ID { get; set; }
   4:    public string Name { get; set; }
   5:    public string Details { get; set; }
   6:    public byte Members { get; set; }
   7:    public string Remarks { get; set; }
   8:  
   9:    /// <summary>
  10:    /// Gets the date and time when this family was last modified.
  11:    /// This date and time is automatically set whenever a new family is saved
  12:    ///  or an existing family has been modified.
  13:    /// </summary>
  14:    /// <value>The last modified.</value>
  15:    public DateTime LastModified { get; set; }
  16:    public Town Town { get; set; }
  17:  
  18:    /// <summary>
  19:    /// Provides the actual family datas as nullable types.
  20:    /// </summary>
  21:    public FamilyFilterAdapter GetAdaptedParams()
  22:    {
  23:       return new FamilyFilterAdapter
  24:                 {
  25:                    Details = Details,
  26:                    Members = Members,
  27:                    ID = ID,
  28:                    LastModified = LastModified,
  29:                    Name = Name,
  30:                    Remarks = Remarks,
  31:                    Town = Town
  32:                 };
  33:    }
  34:  
  35:    #region Implementation of IComparable
  36:  
  37:    public int CompareTo(object obj)
  38:    {
  39:       var otherFamily = obj as Family;
  40:       if(otherFamily != null)
  41:          return Name.CompareTo(otherFamily.Name);
  42:       throw new ArgumentException("Object is not a Family");
  43:    }
  44:  
  45:    #endregion
  46: }
   1: public class FamilyFilterAdapter
   2: {
   3:    public Guid? ID { get; set; }
   4:    public string Name { get; set; }
   5:    public string Details { get; set; }
   6:    public byte? Members { get; set; }
   7:    public string Remarks { get; set; }
   8:  
   9:    /// <summary>
  10:    /// Gets the date and time when this family was last modified.
  11:    /// This date and time is automatically set whenever a new family is saved
  12:    ///  or an existing family has been modified.
  13:    /// </summary>
  14:    /// <value>The last modified.</value>
  15:    public DateTime? LastModified { get; set; }
  16:    public Town Town { get; set; }
  17: }

Und so sieht GetFamily aus:

   1: public static Family GetFamily(FamilyFilterAdapter filter)
   2: {
   3:    var query = Families.AsQueryable()
   4:       .ById(filter.ID)
   5:       .ByName(filter.Name)
   6:       .ByDetails(filter.Details)
   7:       .ByLastModified(filter.LastModified)
   8:       .ByMembers(filter.Members)
   9:       .ByRemarks(filter.Remarks)
  10:       .ByTown(filter.Town);
  11:    if(query.Count() > 1)
  12:       throw new ArgumentException("Es wurde mehr als eine Familie gefunden,"
  13:                                   + " die den Suchkriterien entspricht.");
  14:    Family tempFamily;
  15:    try
  16:    {
  17:       tempFamily = query.First(); // Wirft InvalidOperationException wenn nichts gefunden wurde.
  18:    }
  19:    catch
  20:    {
  21:       return null;
  22:    }
  23:    return tempFamily;
  24: }

Um nun eine Familie zu finden, legt man einfach eine Instanz des FamilyFilterAdapters an, befüllt sie beliebig und übergibt sie an die Methode:

   1: [TestMethod]
   2: public void ShouldFindFamily()
   3: {
   4:    Town town = Repository.GetTown("Teststadt");
   5:    var familyFilter = new FamilyFilterAdapter
   6:                                          {
   7:                                             Name = "Wawasna",
   8:                                             Details = "bla bla",
   9:                                             Town = town
  10:                                          };
  11:    Family result = Repository.GetFamily(familyFilter);
  12:    Assert.AreEqual(familyFilter.Name, result.Name);   
  13: }

GetFamily kann aber auch direkt vom Presentation Layer angesprochen werden, z.B. durch Auswahl in einer Gridview. Nur kommt der Aufruf nicht mit einem Objekt vom Typ FamilyFilterAdapter daher, sondern ist vom Typ Family. Dummerweise müßte also erst eine Instanz von FamilyFilterAdapter geschrieben werden, um sie mit dem Inhalt des Family-Objekts zu füllen. Das ist umständlich und darum habe ich diese Arbeit im Vorfeld erledigt. Oben in der Family-Klasse sieht man eine Methode “GetAdaptedParams()”. Sie erledigt genau diese Arbeit. Der Aufruf von GetFamily geschieht damit so (diese Methode steht in der Repository):

   1: public static bool UpdateFamily(Family family)
   2: {
   3:    // Die ID wird nicht verändert,
   4:    // also können die alten Familiendaten darüber aufgelöst werden.
   5:    Family oldFamilyData = GetFamily(family.ID);
   6:    family.LastModified = DateTime.Now;
   7:    if(GetFamily(oldFamilyData.GetAdaptedParams()) == null)
   8:       return false;
   9:    RemoveFamily(oldFamilyData);
  10:    Families.Add(family);
  11:    SaveFamilies(Families);
  12:    return true;
  13: }

GetFamily(Guid id) ist die einzige Überladung der GetFamily-Methode.

Zum Schluß noch die FluentFilter-Klasse für alle die es interessiert. Ist ein Parameter null, so wird die Query unbehandelt zurück gegeben und damit wird der nächste Filter durchlaufen.

   1: using System;
   2: using System.Linq;
   3:  
   4: namespace Sims3Residents.View
   5: {
   6:    public static class FamilyFluentFilter
   7:    {
   8:       public static IQueryable<Family> ById(this IQueryable<Family> query, Guid? id)
   9:       {
  10:          if(id == null || id.Equals(Guid.Empty))
  11:             // Die ID ist nicht unbedingt bekannt.
  12:             return query;
  13:          var result = query.Where(family => family.ID.Equals(id));
  14:          return result;
  15:       }
  16:  
  17:       public static IQueryable<Family> ByName(this IQueryable<Family> query, string name)
  18:       {
  19:          if(String.IsNullOrEmpty(name))
  20:             throw new ArgumentNullException("name", "Es muß ein Familienname angegeben werden.");
  21:          /* Die Testbarkeit wird verbessert wenn man die query nicht unmittelbar zurück gibt,
  22:           * sondern sie vorher in einer Variablen ablegt. */
  23:          var result = query.Where(family => family.Name.Contains(name));
  24:          return result;
  25:       }
  26:  
  27:       public static IQueryable<Family> ByDetails(this IQueryable<Family> query, string details)
  28:       {
  29:          if(String.IsNullOrEmpty(details))
  30:             // Nicht jede Familie hat Detailinformationen.
  31:             return query;
  32:          var result = query.Where(family => family.Details.Contains(details));
  33:          return result;
  34:       }
  35:  
  36:       public static IQueryable<Family> ByLastModified(this IQueryable<Family> query, DateTime? lastModified)
  37:       {
  38:          if(lastModified == null)
  39:             // Es kann passieren daß man eine Familie sucht, dessen Modifizierungsdatum man nicht kennt.
  40:             return query;
  41:          var result = (query.Where(family => family.LastModified == lastModified));
  42:          return result;
  43:       }
  44:  
  45:       public static IQueryable<Family> ByMembers(this IQueryable<Family> query, byte? members)
  46:       {
  47:          if(members == null || members < 1)
  48:             // Es kann passieren daß man eine Familie sucht, deren Mitgliederzahl man nicht kennt.
  49:             return query;
  50:          var result = (query.Where(family => family.Members == members));
  51:          return result;
  52:       }
  53:  
  54:       public static IQueryable<Family> ByRemarks(this IQueryable<Family> query, string remarks)
  55:       {
  56:          if(string.IsNullOrEmpty(remarks))
  57:             return query;
  58:          var result = (query.Where(family => family.Remarks == remarks));
  59:          return result;
  60:       }
  61:  
  62:       public static IQueryable<Family> ByTown(this IQueryable<Family> query, Town town)
  63:       {
  64:          if(town == null || String.IsNullOrEmpty(town.Name))
  65:             // Es kann passieren daß man eine Familie sucht, deren Wohnort man nicht kennt.
  66:             return query;
  67:          var result = query.Where(family => family.Town.Name.Contains(town.Name));
  68:          return result;
  69:       }
  70:    }
  71: }
DotNetKicks-DE Image
Published Samstag, 2. Januar 2010 22:01 von Rainer Hilmer

Kommentare

# re: Fluent Filter und Domain POCO Adapter

Freitag, 8. Januar 2010 10:20 von Rainer Hilmer

234 Hits und niemand hat eine Meinung dazu?

# re: CCD: Domain POCO Adapter für fluent Filter

Freitag, 22. Januar 2010 21:47 von Mario

Wenn Du noch keine Antwort zu Deinem Problem hast, schau doch mal bei <a href="codekicker.de/.../a> vorbei und stellt dort Deine Frage erneut, vielleicht kann Dir da jemand helfen.

btw: Ich würde mir hier auch ein Filterobjekt bauen.

# re: CCD: Domain POCO Adapter für fluent Filter

Dienstag, 26. Januar 2010 10:35 von Rainer Hilmer

Die CompareTo-Methode vergleicht nur den Namen. Hier ist de Korrektur:

     /// <summary>

     /// Vergleicht zwei Instanzen einer Familie.

     /// </summary>

     /// <returns>

     /// Die Anzahl ungleicher Objekte zwischen den zwei Familien-Instanzen,

     ///  oder -1 falls das Vergleichsobjekt nicht vom Typ Family ist.

     /// </returns>

     public int CompareTo(object obj)

     {

        var otherFamily = obj as Family;

        if(otherFamily == null)

           return -1;

        /* Theoretisch besteht die Möglichkeit, daß eine gleiche Anzahl an

         * negativen und positiven Werten herauskommt

         * und somit in der Gesamtheit 0 ergibt, was Gleichheit bedeuten würde.

         * Um dem vorzubeugen, lasse ich keine Ergebnisse mit negativem Vorzeichen zu. */

        int result = Math.Abs(this.ID.CompareTo(otherFamily.ID));

        result += CompareForNullString(this.Name, otherFamily.Name);

        result += CompareForNullString(this.Details, otherFamily.Details);

        result += Math.Abs(this.Members.CompareTo(otherFamily.Members));

        result += Math.Abs(this.LastModified.CompareTo(otherFamily.LastModified));

        result += CompareForNullString(this.Remarks, otherFamily.Remarks);

        result += CompareTowns(this.Town, otherFamily.Town);

        return result;

     }

     #region Helper methods for CompareTo

     private int CompareForNullString(string stringToCheck, string otherStringToCheck)

     {

        int result = 0;

        if(stringToCheck != null)

           result += Math.Abs(this.Name.CompareTo(otherStringToCheck));

        else

        {

           if(otherStringToCheck != null)

              result += 1;

        }

        return result;

     }

     private static int CompareTowns(Town town, Town otherTown)

     {

        if(town == null && otherTown == null)

           return 0;

        /* otherTown kann sehr wohl null sein. Ich weiß nicht wieso R# auf die Idee kommt,

         * das könnte nicht sein. */

        // ReSharper disable ConditionIsAlwaysTrueOrFalse

        if(town == null && otherTown != null)

           // ReSharper restore ConditionIsAlwaysTrueOrFalse

           return 1;

        return town.Equals(otherTown) ? 0 : 1;

     }

# re: CCD: Domain POCO Adapter für fluent Filter

Dienstag, 26. Januar 2010 10:41 von Rainer Hilmer

Danke für die Korrektur, Rainer. Du bist der erste von 421 dem das aufgefallen ist. ;-)

# re: CCD: Domain POCO Adapter für fluent Filter

Dienstag, 26. Januar 2010 10:44 von Rainer Hilmer

Gern geschehen, Rainer. :-)

Kommentar abgeben

(verpflichtend) 
(verpflichtend) 
(optional)
(verpflichtend)