Exposing WCF Service with dynamic search operation

Posted on

Problem

I have this WCF service providing search functionality – to be used by our client in his web application. There can be any number of search parameters and possible nesting of AND/OR operations.

Could you please have a look and share some ideas on if I can make it more client-friendly?

  <con:Search>
     <con:clientID>xxx</con:clientID>
     <con:propertyQueries>
        <wil:PropertyQuery Id="0">
            <wil:PropertyID>DocumentType</wil:PropertyID>      
            <wil:Value>Statement</wil:Value>
            <wil:Operator>EQUALS</wil:Operator> 
        </wil:PropertyQuery>        
        <wil:PropertyQuery Id="1" AND="0">
            <wil:PropertyID>BANK</wil:PropertyID>      
            <wil:Value>HSBC</wil:Value>             
            <wil:Operator>EQUALS</wil:Operator> 
        </wil:PropertyQuery>        
        <wil:PropertyQuery Id="2" OR="1">
            <wil:PropertyID>BANK</wil:PropertyID>      
            <wil:Value>BARCLAYS</wil:Value>
            <wil:Operator>EQUALS</wil:Operator> 
        </wil:PropertyQuery>    
        <wil:PropertyQuery Id="3" AND="0">
            <wil:PropertyID>Date</wil:PropertyID>      
            <wil:Value Start="1">11/01/2014</wil:Value>
            <wil:Value End="1">11/02/2014</wil:Value>
            <wil:Operator>BETWEEN</wil:Operator>    
        </wil:PropertyQuery>            
    </con:propertyQueries>
  </con:Search>   

The values in AND and OR attribute indicates the Id of PropertyQuery to which the condition to be added. For example:

<wil:PropertyQuery Id="2" OR="1">

performs an OR operation between Property Queries with ID of 1, 2.

Any better ideas out there to improve the interface?

Solution

The way you’re composing expressions seems very unintuitive to me. I think that you should take advantage of the fact that you’re trying to express (part of) the SQL syntax tree in the XML tree. So, AND should be a separate node and it should have its child expressions as child nodes. Something like (if I misunderstood how your AND/ORs work, that only proves my point):

<wil:combinedQuery operator="AND">
    <wil:combinedQuery operator="AND">
        <wil:PropertyQuery Id="0">
            <wil:PropertyID>DocumentType</wil:PropertyID>      
            <wil:Value>Statement</wil:Value>
            <wil:Operator>EQUALS</wil:Operator> 
        </wil:PropertyQuery>
        <wil:PropertyQuery Id="3">
            <wil:PropertyID>Date</wil:PropertyID>      
            <wil:Value Start="1">11/01/2014</wil:Value>
            <wil:Value End="1">11/02/2014</wil:Value>
            <wil:Operator>BETWEEN</wil:Operator>    
        </wil:PropertyQuery> 
    </wil:combinedQuery>
    <wil:combinedQuery operator="OR">
        <wil:PropertyQuery Id="1" AND="0">
            <wil:PropertyID>BANK</wil:PropertyID>      
            <wil:Value>HSBC</wil:Value>             
            <wil:Operator>EQUALS</wil:Operator> 
        </wil:PropertyQuery>        
        <wil:PropertyQuery Id="2" OR="1">
            <wil:PropertyID>BANK</wil:PropertyID>      
            <wil:Value>BARCLAYS</wil:Value>
            <wil:Operator>EQUALS</wil:Operator> 
        </wil:PropertyQuery>   
    </wil:combinedQuery>
</wil:combinedQuery>

Dates should be in the unambiguous ISO 8601 format, not in the ambiguous and confusing (mm/dd/yyyy, or is it dd/mm/yyyy?) format.


I don’t like much the way you’re representing BETWEEN. I think that Start and End should be in elements (or attributes) with those names.

Without knowing its intended audience (Intranet / Internet), I’m assuming Intranet.

In my opinion, you are giving a lot of flexibility to this service in one method call, which isn’t immediately clear how a user would structure a call to this. The caller has to have knowledge of a “propertyId” which doesn’t make too much sense on a glance. I hope your API documentation describes this well, and how the user would retrieve what these IDs are.

So here is my assumption of the coded interface:

public interface ISearchService
{
    List<object> Search(string clientId, IEnumerable<PropertyQuery> propertyQueries);
}

// Element
public class PropertyQuery
{
   // Attribute
   public string PropertyID { get; set; }

   // Array
   public IEnumerable<Value> Values { get; set; }

   // Attribute
   public byte AND { get; set; }

   // Attribute
   public byte OR { get; set; }

   // Element
   public string Operator { get; set; }
}

// Element
public class Value
{
   // Attribute
   public int Id { get; set; }

   // Attribute
   public byte Start { get; set; }

   // Attribute
   public byte End { get; set; }

   // Text
   public string Text { get; set; }
}

To make this a little simpler, I would add a single class that takes all parameters and a class returning the results, following the Request/Response pattern. This means that the interface can change over time, and should not affect backwards compatibility.

public interface ISearchService
{
   // Gets the search criteria for the screen
   SearchCriteriaResponse GetSearchCriterias(SearchCriteriaRequest request);

   // Searches by the search criteria
   SearchResponse SearchBy(SearchRequest searchCriteria);
}

// Base elements for a response
public abstract class ResponseBase
{
   public bool Success { get; set; }
   public string Message { get ; set; }
}

public class SearchCriteriaResponse : ResponseBase
{
   public string[] DocumentTypes { get; set; }
   public string[] BankName { get; set; }
}

public class SearchResponse : ResponseBase
{
   public IEnumerable<SearchResult> Results { get; set; }
}

public class SearchRequest
{
   public string ClientId { get; set; }
   public string[] BankNamesToMatchExactly { get; set; }
   public string[] BankNamesToPartiallyExactly { get; set; }
   public string[] DocumentTypesToMatchExactly { get; set; }
   public string[] DocumentTypesToPartiallyExactly { get; set; }
   public DateTime? StartDate { get; set; }
   public DateTime? EndDate { get;set; }
   public bool CheckBetweenDates { get; set; }
}

This interface is a more clean on its approach. Its slightly larger, but it makes the ORing and ANDing much more obvious. It also makes the code cleaner at the service side as each of these properties have well-named properties and should be easy to implement:

public List<SearchResponse> SearchBy(SearchRequest searchCriteria)
{
   IQueryable dbQuery = DbContext.BankNames;

   foreach(var bankName in searchCriteria.BankNamesToMatchExactly)
   {
      dbQuery = dbQuery.Where(bn => bn.BankName == bankName);
   }

   foreach(var bankName in searchCriteria.BankNamesToMatchPartially)
   {
      dbQuery = dbQuery.Where(bn => bn.BankName.Contains(bankName);
   }

   // etc..
}

Leave a Reply

Your email address will not be published. Required fields are marked *