samedi 11 février 2017

C#: custom IEnumerable.Join() based on complex match

I found myself doing a lot of merging of lists based on complex criterias.



Let see an example:
Given the classes as follow:

  class Person  
   {  
     public string FullName { get; set; }  
     public string ClinicAddress { get; set; }  
     public string City { get; set; }  
     public string Gendre { get; set; }  
   }  
   class Clinic  
   {  
     public string Address { get; set; }  
     public string City { get; set; }  
     public List<Treatment> AcceptedTreatments { get; set; }  
   }  

Then I have a List<Person> and a List<Clinic> and here is my problem:

For each Person I want to find a clinic on same city accepting its medical records and then fullfill the property person.ClinicAddress.

Basically, we need to find matching elements and then join them.

Of course there is a Linq operator for that: IEnumerable.Join(), it works based on keys on both lists to do the match and executing a Func<> receiving both matching elements to do the join.

But it was not exactly what I was looking for. In most of my cases I don't have evident keys to match on both sides but a complex decision criteria. Also, in some cases, if no match found, I want to assign a default value.

Here is some sample code to have an idea of the problem:

 var persons = new List<Person>();   
 var clinics = new List<Clinic>();  
 foreach (var person in persons)  
 {  
    var mh = GetMedicalHistory(person);   
    var match = clinics.FirstOrDefault(a => CanAssignClinic(mh, a);  
    if (match != null)  
    {  
        person.ClinicAddress = $"{match.Address}, {match.City}";  
    }
    else
    {
        person.ClinicAddress = "Hospital on the Capital";
    }
 }  

Lets just say that methods GetMedicalHistory() and CanAssignClinic() are not so simple to represent with a match of properties, which makes difficult to pass matching values to keySelector parameters of IEnumerable.Join().

I found the same pattern in a lot of places on my code, the pattern is as follow:

For each element of target list, apply some criteria over all elements of second list, if found a match, then do whatever I want with matching elements, if no match, then do something else.

Event if IEnumerable.Join() was possible to apply in some cases I decided to go with a custom implementation more readable and simple to represent, also to have the possibility to deal with no matching found situation, so I ended up with this:

 public static void JoinByCriteria<T1, T2>(this IEnumerable<T1> target,   
       IEnumerable<T2> source,   
       Func<T1, T2, bool> condition,   
       Action<T1, T2> whenMatchFoundOperation,  
       Action<T1> whenMatchNotFoundOperation = null)  
     {  
       foreach (var t in target)  
       {  
         var match = source.FirstOrDefault(s => condition(t, s));  
         if (match != null)  
         {  
           whenMatchFoundOperation(t, match);  
         }  
         else if (whenMatchNotFoundOperation != null)                  
         {  
           whenMatchNotFoundOperation(t);  
         }  
        }  
     }  

It is an extension method for IEnumerable, and here is a way to use it:

 persons.JoinByCriteria(clinics, (person, clinic) =>  
       {  
         var mh = GetMedicalHistory(person);  
         return CanAssignClinic(mh, clinic);  
       },  
       (person, agency) =>   
       {  
         person.ClinicAddress = agency.Address;  
       },  
       (person) =>  
       {  
         person.ClinicAddress = "City Hospital";  
       });  

Aucun commentaire:

Enregistrer un commentaire