LINQ Coding Guidelines #8–Provider nicht mischen

19. März 2015

Der LINQ-Provider hat nicht unerheblichen Einfluss auf die Ausführung der LINQ-Ausdrücke. Sich dessen bewusst zu sein ist wichtig, diesen Umstand für alle Leser deutlich zu machen nicht minder.

Empfehlung: LINQ-Abfragen die in relevanten Anteilen unterschiedliche Provider betreffen sollten entsprechend getrennt werden.

 

Fangen wir wieder mit einem Beispiel an:

   1: public static IEnumerable<object> GetSystemsForApplication(this SystemRepository repository, string applicationId, bool includeWithdrawn)

   2: {

   3:     return (from item in repository.ServersThisApplicationDependsOn

   4:             where (item.CONSUMING_APPLICATION == applicationId) && (includeWithdrawn || (item.DEPENDENCY_STATUS != WithDrawnIdentifier))

   5:             orderby item.SERVER_NAME

   6:             select item).AsEnumerable().Select(item => new

   7:             {

   8:                 item.CONSUMING_APPLICATION,

   9:                 item.SERVER_NAME,

  10:                 item.DEPENDENCY_STATUS,

  11:                 item.SERVER_STATUS,

  12:                 item.SERVER_OS,

  13:                 item.SERVER_LOCATION,

  14:                 item.SERVER_FQDN,

  15:                 LATEST_TARGET_HARDWARE_REMOVAL_DATE = GetFormatedNullableDateTime(item.LATEST_TARGET_HARDWARE_REMOVAL_DATE)

  16:             });

  17: }

Gesehen?

Der expliziten Aufruf von AsEnumerable() führt dazu, dass der erste Teil der Abfrage gegen das Entity Framework ausgeführt wird, der nachfolgende Teil hingegen gegen LINQ-to-Objects. Im vorliegenden Fall bleibt das weitgehend ohne Konsequenzen, ich habe aber auch schon Beispiele gesehen, wo das nicht der Fall ist. Zumindest dürfte nicht jedem Entwickler sofort ersichtlich sein, was hier passiert – die Formatierung tut ein Übriges um das untergehen zu lassen.

 

Ein weiteres Beispiel ist die Suchfunktion die ich in einem der letzten Beiträge schon gezeigt habe:

   1: public Collection<CashTransactionBE> SearchTransactions(CashTransactionSearchCriteriaBE searchCriteria, long accountId)

   2: {

   3:     using (EvolutionDBEntities entities = new EvolutionDBEntities())

   4:     {                

   5:         var condition = LinqHelper.True<Transaction>();

   6:         var conditionBalance = condition.And(t => (((searchCriteria.MinimumAmount == null) || (t.Amount >= searchCriteria.MinimumAmount)) &&

   7:                                                   ((searchCriteria.MaximumAmount == null) || (t.Amount <= searchCriteria.MaximumAmount))));

   8:         var conditionDate = conditionBalance.And(t => (((searchCriteria.StartDate == null) || (t.Date > searchCriteria.StartDate.Value.AddDays(-1))) &&

   9:                                                       ((searchCriteria.EndDate == null) || (t.Date < searchCriteria.EndDate.Value.AddDays(1)))));

  10:         string transactionType = GetTransactionType(searchCriteria.CashTransactionType);

  11:         var conditionTransactionType = conditionDate.And(t => ((searchCriteria.CashTransactionType == TransactionType.All) || (transactionType == t.Type)));

  12:  

  13:         IEnumerable<Transaction> result = entities.Transaction

  14:             .Where(t => t.AccountID == accountId)

  15:             .Where(conditionTransactionType.Compile());

  16:  

  17:         return result.Select(t => t.ToBE()).ToCollection();

  18:     }

  19: }

Na?

Dieser Fall ist schon hinterhältiger: Die erste Filterung nach der AccountID findet noch gegen das Entity Framework, d.h. effizient in der Datenbank, statt. Der zweite Filter (mit condition.Compile()) geht jedoch nicht gegen Queryable, sondern gegen Enumerable, weil Compile() keine Expression, sondern einen Delegate liefert. (Kein neues Thema.) Den Filter nicht in der Datenbank sondern im Speicher anzuwenden war hier sicher nicht im Sinne des Erfinders. Oder doch? Ist die erwartete Datenmenge so gering, dass das hier keinen Unterschied macht? Wir wissen es nicht, genauso wenig wie der Entwickler, der den Code irgendwann übernehmen und pflegen muss – und der jetzt vor einem Wartungsproblem steht.

 

In beiden Beispielen findet bei der Abfrage ein Wechsel des Providers statt. Und in beiden Fällen ist der Leser mit der Frage alleine gelassen, ob sich der Autor der Problematik bewusst war. Schlimmer: bei einer späteren Anpassung des Codes könnte ein Entwickler zum Zuge kommen, der sich der Problematik nicht bewusst ist und der das Kind nachträglich durch Änderungen in den Brunnen schubst.

Ergo sollten solche Wechsel des Providers im Code deutlich nachvollziehbar dokumentiert werden, etwa:

   1: public static IEnumerable<object> GetSystemsForApplication(this SystemRepository repository, string applicationId, bool includeWithdrawn)

   2: {

   3:     var result= (from item in repository.ServersThisApplicationDependsOn

   4:             where (item.CONSUMING_APPLICATION == applicationId) && (includeWithdrawn || (item.DEPENDENCY_STATUS != WithDrawnIdentifier))

   5:             orderby item.SERVER_NAME

   6:             select item);

   7:     // wegen Hilfsfunktion beim Mapping im Speicher durchführen:

   8:     return result.AsEnumerable().Select(item => new

   9:             {

  10:                 item.CONSUMING_APPLICATION,

  11:                 item.SERVER_NAME,

  12:                 item.DEPENDENCY_STATUS,

  13:                 item.SERVER_STATUS,

  14:                 item.SERVER_OS,

  15:                 item.SERVER_LOCATION,

  16:                 item.SERVER_FQDN,

  17:                 LATEST_TARGET_HARDWARE_REMOVAL_DATE = GetFormatedNullableDateTime(item.LATEST_TARGET_HARDWARE_REMOVAL_DATE)

  18:             });

  19: }

Und:

   1: IEnumerable<Transaction> result = entities.Transaction.Where(t => t.AccountID == accountId);

   2: // Abfrage im Speicher! (Aufgrund der geringen Datenmenge vertretbar!)

   3: result = result.Where(conditionTransactionType.Compile());

Jetzt sind die Unklarheiten beseitigt.

Leider ist an den Aufrufen selbst nicht erkennbar, ob sie gegen Queryable oder Enumerable gehen. Warnsignale sind notwendige Aufrufe von AsQueryable() oder AsEnumerable(). Der implizite Wechsel durch den Typ des Delegates ist hinterhältiger, aber glücklicherweise auch seltener.