
Adding More Complex Query Capabilities
The provider that you have to this point provides a very limited way for clients to specify location information in the LINQ query. Specifically, the provider is only able to obtain location information from equality expressions such as Place.Name == "Seattle" or Place.State == "Alaska" (Place.Name = "Seattle" or Place.State = "Alaska" in Visual Basic).
The next procedure shows you how to add support for an additional way of specifying location information. When you have added this code, your provider will be able to extract location information from method call expressions such as place.Name.StartsWith("Seat").
To add support for predicates that contain String.StartsWith
In the LinqToTerraServerProvider project, add the VisitMethodCall method to the LocationFinder class definition.
Protected Overrides Function VisitMethodCall(ByVal m As MethodCallExpression) As Expression
If m.Method.DeclaringType Is GetType(String) And m.Method.Name = "StartsWith" Then
If ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "Name") Or _
ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then
_locations.Add(ETH.GetValueFromExpression(m.Arguments(0)))
Return m
End If
End If
Return MyBase.VisitMethodCall(m)
End Function
protected override Expression VisitMethodCall(MethodCallExpression m)
{
if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith")
{
if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") ||
ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State"))
{
locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0]));
return m;
}
}
return base.VisitMethodCall(m);
}
Recompile the LinqToTerraServerProvider project.
To test the new capability of your provider, open the file Program.cs (or Module1.vb in Visual Basic) in the ClientApp project. Replace the code in the Main method with the following code:
QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
var query = from place in terraPlaces
where place.Name.StartsWith("Lond")
select new { place.Name, place.State };
foreach (var obj in query)
Console.WriteLine(obj);
Dim terraPlaces As New QueryableTerraServerData(Of Place)
Dim query = From place In terraPlaces _
Where place.Name.StartsWith("Lond") _
Select place.Name, place.State
For Each obj In query
Console.WriteLine(obj)
Next
Run the program and view the results. There should be approximately 29 results.
The next procedure shows you how to add functionality to your provider to enable the client query to specify location information by using two additional methods, specifically Enumerable..::.Contains and List<(Of <(T>)>)..::.Contains. When you have added this code, your provider will be able to extract location information from method call expressions in the client query such as placeList.Contains(place.Name), where the placeList collection is a concrete list supplied by the client. The advantage of letting clients use the Contains method is that they can specify any number of locations just by adding them to placeList. Varying the number of locations does not change the syntax of the query.
To add support for queries that have the Contains method in their 'where' clause
In the LinqToTerraServerProvider project, in the LocationFinder class definition, replace the VisitMethodCall method with the following code:
Protected Overrides Function VisitMethodCall(ByVal m As MethodCallExpression) As Expression
If m.Method.DeclaringType Is GetType(String) And m.Method.Name = "StartsWith" Then
If ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "Name") Or _
ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then
_locations.Add(ETH.GetValueFromExpression(m.Arguments(0)))
Return m
End If
ElseIf m.Method.Name = "Contains" Then
Dim valuesExpression As Expression = Nothing
If m.Method.DeclaringType Is GetType(Enumerable) Then
If ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "Name") Or _
ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "State") Then
valuesExpression = m.Arguments(0)
End If
ElseIf m.Method.DeclaringType Is GetType(List(Of String)) Then
If ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "Name") Or _
ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "State") Then
valuesExpression = m.Object
End If
End If
If valuesExpression Is Nothing OrElse valuesExpression.NodeType <> ExpressionType.Constant Then
Throw New Exception("Could not find the location values.")
End If
Dim ce = CType(valuesExpression, ConstantExpression)
Dim placeStrings = CType(ce.Value, IEnumerable(Of String))
' Add each string in the collection to the list of locations to obtain data about.
For Each place In placeStrings
_locations.Add(place)
Next
Return m
End If
Return MyBase.VisitMethodCall(m)
End Function
protected override Expression VisitMethodCall(MethodCallExpression m)
{
if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith")
{
if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") ||
ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State"))
{
locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0]));
return m;
}
}
else if (m.Method.Name == "Contains")
{
Expression valuesExpression = null;
if (m.Method.DeclaringType == typeof(Enumerable))
{
if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "Name") ||
ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "State"))
{
valuesExpression = m.Arguments[0];
}
}
else if (m.Method.DeclaringType == typeof(List<string>))
{
if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "Name") ||
ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "State"))
{
valuesExpression = m.Object;
}
}
if (valuesExpression == null || valuesExpression.NodeType != ExpressionType.Constant)
throw new Exception("Could not find the location values.");
ConstantExpression ce = (ConstantExpression)valuesExpression;
IEnumerable<string> placeStrings = (IEnumerable<string>)ce.Value;
// Add each string in the collection to the list of locations to obtain data about.
foreach (string place in placeStrings)
locations.Add(place);
return m;
}
return base.VisitMethodCall(m);
}
This method adds each string in the collection that Contains is applied to, to the list of locations to query the Web service with. A method named Contains is defined in both Enumerable and List<(Of <(T>)>). Therefore, the VisitMethodCall method must check for both of these declaring types. Enumerable.Contains is defined as an extension method; therefore the collection it is applied to is actually the first argument to the method. List.Contains is defined as an instance method; therefore the collection it is applied to is the receiving object of the method.
Recompile the LinqToTerraServerProvider project.
To test the new capability of your provider, open the file Program.cs (or Module1.vb in Visual Basic) in the ClientApp project. Replace the code in the Main method with the following code:
QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
string[] places = { "Johannesburg", "Yachats", "Seattle" };
var query = from place in terraPlaces
where places.Contains(place.Name)
orderby place.State
select new { place.Name, place.State };
foreach (var obj in query)
Console.WriteLine(obj);
Dim terraPlaces As New QueryableTerraServerData(Of Place)
Dim places = New String() {"Johannesburg", "Yachats", "Seattle"}
Dim query = From place In terraPlaces _
Where places.Contains(place.Name) _
Order By place.State _
Select place.Name, place.State
For Each obj In query
Console.WriteLine(obj)
Next
Run the program and view the results. There should be approximately 5 results.