Linq2Rest
I needed to create a .NET WebApi server project that could support calling the API directly with linq.
Note that the server would have been using entity framework, so the linq calls made to the api would
have been directly converted into Linq2Sql
On the client side i needed not to use the standard integration via the service references. I needed
a more "personal" and manageable way, to add headers and to enrich the request with special authentication
informations.
On the server side
This follows more or less the approach given by Microsoft on the subject
Several packages must be added on the server side via nuget
- Microsoft.AspNet.OData
- Microsoft.AspNet.WebApi
- Microsoft.AspNet.WebApi.OData
- Microsoft.Data.OData
- Newtonsoft.Json
On Global.asax this must be added to return json data usable by others than microsoft.
protected void Application_Start()
{
var jSettings = new JsonSerializerSettings();
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings = jSettings;
GlobalConfiguration.Configuration.AddODataQueryFilter();
}
The controller methods supporting IQueryable must be changed.
From
[HttpGet]
public IEnumerable<CustomerContract> ListItems(string sessionId,int skip = 10,int take = 10,string name = null)
{
if(_sessionService.IsSessionValid(sessionId)){
using(var context = new ContosoDbContext()){
if(!string.IsNullOrWhiteSpace(name)){
return context.Customers.Where(c=>c.Name==name).Skip(skip).Take(10);
}else{
return context.Customers.Skip(skip).Take(10);
}
}
}else{
throw new InvalidSessionException();
}
}
Will be changed
- Decorating with EnableQuery attribute
- Returning IQueryable
- All parameters relative to the query, like filters, pagination and ordering
must be removed
[HttpGet]
[EnableQuery]
public IQueryable<CustomerContract> ListItems(string sessionId)
{
if(_sessionService.IsSessionValid(sessionId)){
using(var context = new ContosoDbContext()){
return context.Customers;
}
}else{
throw new InvalidSessionException();
}
}
BEWARE THAT...
...returning something with "ToList" or "ToArray" will remove the performance
benefits or IQueryable: calling them will actually EXECUTE the query and get
all results, that will be then filtered through Linq2Objects.
You will gain always in simplicity since the WebApi will take care of pagination
and filtering.
On the client side
We should take 2 approaches, the standard from Microsoft with Service references
or we can manage directly the queries via Linq2Rest
Unluckily it is not explained how could we insert additional headers (like for...authentication)
or cookies and i did a bit of reverse engineering on the subject obtaining this result
Several packages must be added on the client side via nuget
- Linq2Rest
- Newtonsoft.Json
Then we should mimic the basic infrastructure of Linq2Rest
Request adapter
public class MyHttpWebRequestAdapter : IHttpRequest
{
private readonly HttpWebRequest _httpWebRequest;
public MyHttpWebRequestAdapter(HttpWebRequest httpWebRequest)
{
_httpWebRequest = httpWebRequest;
}
public static HttpWebRequest CreateHttpWebRequest(Uri uri, HttpMethod method, string responseMimeType, string requestMimeType)
{
requestMimeType = requestMimeType ?? responseMimeType;
var httpWebRequest = (HttpWebRequest)WebRequest.Create(uri);
httpWebRequest.Method = ((object)method).ToString().ToUpperInvariant();
if (method == HttpMethod.Post || method == HttpMethod.Put)
{
httpWebRequest.ContentType = requestMimeType;
}
httpWebRequest.Accept = responseMimeType;
return httpWebRequest;
}
public Stream GetRequestStream()
{
return ((WebRequest)_httpWebRequest).GetRequestStream();
}
public Stream GetResponseStream()
{
return ((WebRequest) _httpWebRequest).GetResponse().GetResponseStream();
}
}
Json serializer
public class JsonNetSerializer<T> : ISerializer<T>
{
public T Deserialize(string input)
{
return JsonConvert.DeserializeObject<T>(input);
}
public IList<T> DeserializeList(string input)
{
return JsonConvert.DeserializeObject<IList<T>>(input);
}
public T Deserialize(Stream input)
{
var serializer = new JsonSerializer();
using (var sr = new StreamReader(input))
using (var jsonTextReader = new JsonTextReader(sr))
{
return serializer.Deserialize<T>(jsonTextReader);
}
}
public IEnumerable<T> DeserializeList(Stream input)
{
var serializer = new JsonSerializer();
using (var sr = new StreamReader(input))
using (var jsonTextReader = new JsonTextReader(sr))
{
return serializer.Deserialize<IList<T>>(jsonTextReader);
}
}
public Stream Serialize(T item)
{
throw new NotImplementedException();
}
}
Request factory
public class MyRequestFactory : IHttpRequestFactory
{
public IHttpRequest Create(Uri uri, HttpMethod method, string responseMimeType, string requestMimeType)
{
var request = MyHttpWebRequestAdapter.CreateHttpWebRequest(uri, method, responseMimeType, requestMimeType);
var appName = Configuration.ApplicationSection.Current.AppName;
var appSecret = Configuration.ApplicationSection.Current.Secret;
request.Headers[HttpRequestHeader.Authorization] = string.Format("MY_SERVER {0}={1}", appName,
appSecret);
return (IHttpRequest)new MyHttpWebRequestAdapter(request);
}
}
Json serializer factory
public class MyJsonNetSerializerFactory : ISerializerFactory
{
public ISerializer<T> Create<T>()
{
return new JsonNetSerializer<T>();
}
public ISerializer<T> Create<T, TSource>()
{
return new JsonNetSerializer<T>();
}
}
Given all this the call to the api can be done this way:
public IQueryable<expectedObject> LoadData()
{
//This parameters will be still considered
var apiUrl = "/api/customer/ListItems?sessionId=123456";
var serializerFactory = new MyJsonNetSerializerFactory();
var requestFactory = new MyRequestFactory();
var jsonRestClient = new JsonRestClient(new Uri(apiUrl),requestFactory);
using (var restContext = new RestContext<expectedObject>(jsonRestClient, serializerFactory))
{
return restContext.Query;
}
}
At this point we could apply the standard linq to the result of LoadData. And the real
call will be made ONLY when the data will be enumerated!