首页 > 代码库 > [转]ASP.NET web API 2 OData enhancements

[转]ASP.NET web API 2 OData enhancements

本文转自:https://www.pluralsight.com/blog/tutorials/asp-net-web-api-2-odata-enhancements

Along with the release of Visual Studio 2013 came a new release of ASP.NET MVC (called MVC 5) and ASP.NET Web API (called Web API 2). Part of the enhancements to the Web API stack include some much needed features for OData that were missing in the initial release of the ASP.NET Web API OData that came out in June 2013. There were four new OData capabilities added in this release: $select, $expand, $value, and batch updates.

A quick review of the basics

OData is an open standard specification for exposing and consuming CRUD-centric (Create-Read-Update-Delete) data services over HTTP. OData follows most of the design guidelines of the REST architectural style and makes it very easy to implement data services or consume them on just about any modern development platform. OData has two main parts to it: the OData URL query syntax that allows you to perform rich queries from the client including filtering, sorting, and paging; and the full OData communication protocol that includes OData message formatting standards and the supported operations and calling patterns for performing CRUD operations via OData.

The first release of Web API OData added support for OData services using ASP.NET Web API. This included support for the OData query syntax for any Web API service that exposes IQueryable collection results via GET operations, as well as implementing an OData protocol compliant service that supports full CRUD operations on an object model with relationships between the entities. Let‘s focus on services that are “full” OData services with Web API.

The first step in using OData within your ASP.NET Web API site is to pull in the ASP.NET Web API 2 OData NuGet package.

技术分享

When defining a Web API OData service that supports CRUD operations, you will generally want to inherit from the EntitySetController<EntityType,KeyType> base class. The base class exposes virtual methods for you to override for each of the CRUD operations mapping to the POST (Create), GET (Retrieve), PUT/PATCH (Update), and DELETE (Delete) HTTP verbs. For Create and Update operations, the base class handles the request and dispatches the call to a virtual method that lets you just deal with the entity being updated or created instead of handling the raw request yourself.

A simple controller dealing with Customers in a database for a Pizza ordering domain (Zza) is shown below.

public class CustomersController : EntitySetController<Customer, Guid>{    ZzaDbContext _Context = new ZzaDbContext();     [Queryable()]    public override IQueryable Get()    {        return _Context.Customers;    }     protected override Customer GetEntityByKey(Guid key)    {        return _Context.Customers.FirstOrDefault(c => c.Id == key);    }     protected override Guid GetKey(Customer entity)    {        return entity.Id;    }     protected override Customer CreateEntity(Customer entity)    {        if (_Context.Customers.Any(c=>c.Id == entity.Id))            throw ErrorsHelper.GetForbiddenError(Request,                 string.Format("Customer with Id {0} already exists", entity.Id));        _Context.Customers.Add(entity);        _Context.SaveChanges();        return entity;    }     protected override Customer UpdateEntity(Guid key, Customer update)    {        if (!_Context.Customers.Any(c => c.Id == key))            throw ErrorsHelper.GetResourceNotFoundError(Request);        update.Id = key;        _Context.Customers.Attach(update);        _Context.Entry(update).State = System.Data.Entity.EntityState.Modified;        _Context.SaveChanges();        return update;    }     protected override Customer PatchEntity(Guid key, Delta patch)    {        var customer = _Context.Customers.FirstOrDefault(c => c.Id == key);        if (customer == null)            throw ErrorsHelper.GetResourceNotFoundError(Request);        patch.Patch(customer);        _Context.SaveChanges();        return customer;    }     public override void Delete(Guid key)    {        var customer = _Context.Customers.FirstOrDefault(c => c.Id == key);        if (customer == null)            throw ErrorsHelper.GetResourceNotFoundError(Request);        _Context.Customers.Remove(customer);        _Context.SaveChanges();    }     protected override void Dispose(bool disposing)    {        _Context.Dispose();        base.Dispose(disposing);    }}

Hand in hand with this, you need to set up your routing for the ASP.NET site to support OData requests and to map them to an Entity Data Model that the OData support can use to expose the metadata about the server data model.

The following code from the WebApiConfig class in the App_Start folder of the ASP.NET project takes care of setting up that routing for this example:

public static class WebApiConfig{    public static void Register(HttpConfiguration config)    {        config.Routes.MapODataRoute("ZzaOData", "zza", GetEdmModel());    }    private static IEdmModel GetEdmModel()    {        var builder = new ODataConventionModelBuilder();        builder.EntitySet("Customers");        builder.EntitySet("Orders");        builder.EntitySet("OrderItems");        builder.EntitySet("Products");        return builder.GetEdmModel();    }}

With that code in place in an ASP.NET site, you can start issuing OData query requests such as the following to get all customers in the state of Virginia ordered by LastName:

GET /zza/Customers()?$filter=State%20eq%20‘VA‘&$orderby=LastName

The payload of the HTTP response would contain the results, formatted using the JSON Light format of the OData specification. OData supports three formats for the payloads – JSON Light, JSON Verbose, and ATOM/XML. The JSON Light format is the default for Web API OData and just contains the basic entities and their properties/values. JSON Verbose is the original JSON format supported by OData version 1.0, but as its current name implies, it is a much larger payload because it contains metadata about the entities as well as hypermedia links for related objects and actions available on the entities. The XML format supported is the ATOM Publishing Protocol, which contains the entities and their properties and values as well as the metadata and hypermedia links.

To Create (insert) a new Customer, you would send an HTTP POST request to /zza/Customers with the Customer entity in the HTTP body using one of the supported formats. To perform an update, you can choose between an HTTP PUT, where you are expected to send the whole entity with all of its properties in the request body, and those will be used to update the entire entity in the persistence store on the back end. For partial updates, you can use the PATCH or MERGE verbs in your request and just send the properties that have changes, and those will be merged with the other existing properties on the back end to perform the update. For a DELETE, you issue the request to /zza/Customers/<id>, where <id> is the primary key identifier for the entity you are deleting.

Of course, if you are writing a .NET client for the OData service, you don’t have to worry about the low level HTTP requests that need to be issued, you can use the WCF Data Services client. This can confuse some people because of the fact that you are not doing anything to do with WCF on the back end if you use Web API OData. But the WCF Data Services Client is decoupled from the server side implementation by the OData wire level protocol – as long as the server is speaking OData, it doesn’t matter what platform it is implemented on. It might not even be Windows, but you can use the WCF Data Services Client to talk to it.

To use the WCF Data Services Client you simply go into the client project in Visual Studio, add the WCF Data Services Client NuGet package, and then invoke the Add Service Reference menu from Solution Explorer on the project. From there you put in the base address for the service (i.e. http://localhost:12345/zza) and it will retrieve the metadata about the service, the exposed entity collections, and the type information about the entity model from the server. It will generate a proxy class called Container for you and then you can write client code like the following to retrieve entities:

DataServiceCollection customers = new DataServiceCollection();var odataProxy = new Container(new Uri("http://localhost.fiddler:39544/zza"));IQueryable query =     from c in odataProxy.Customers where c.State == "VA" select c;customers.Load(query);customersDataGrid.ItemsSource = customers.ToList();

 

The DataServiceCollection will track changes made to the entities, you can add new entities to the collection, then call SaveChanges on the proxy. If you need to delete an entity, you do that with a call to Delete() on the proxy collection itself. You can make multiple changes on the client side and they are all cached in memory by the WCF Data Services Client proxy. When you call SaveChanges() on the proxy, it will send each of the changes down to the server at that point for persistence.

Web API 2 OData changes

As mentioned at the beginning of this post, Web API 2 added four new capabilities to the OData stack in ASP.NET: $select, $expand, $value, and batch update operations. The really cool thing is that for the first three, if you have already been using Web API OData since it was released in June 2012, you don’t need to make any changes to your code. Those new features just “light up” when you add the ASP.NET Web API 2 OData NuGet package as an update to the original one. For the batch update operations, it is just a simple change to your routing initialization and you are off and running with that capability. Let’s take a look at each one briefly.

Retrieving partial entities with $select

The $select operator lets you retrieve a “partial” entity in an OData query. This means you can request just the properties on a given entity type that you care about as a client at a particular point in time. The entity type may contain 50 properties in the server object model, but maybe you just need a few of those to present in a data grid on the client for example. You can issue a $select query that specifies the property names you care about like so:

GET /zza/Customers?$select=FirstName,LastName,Phone

If you are using the WCF Data Services Client in a .NET client application, you could just use the Select LINQ method on your query against the proxy:

var selectQuery = _OdataProxy.Customers.Select(c =>     new { FirstName = c.FirstName, LastName = c.LastName, Phone = c.Phone });

If you inspect the payload that comes back with a tool like Fiddler, you will see that each entity only contains the requested properties. If the underlying provider on the server side that executes the query is a LINQ provider like Entity Framework, it will execute a SELECT statement at the database level that only requests the specific properties. This will improve performance because the database does not need to return unnecessary data to the web server, and it saves bandwidth between the web server and the client application to only transmit what is needed.

Eager loading with $expand

The next new operator is the $expand operator. This lets you request related entities to the root collection you are querying. For example, if I am querying Customers, but I also want to retrieve the Orders for each Customer, as well as possibly the OrderItems for each Order, and maybe even the related Product for each OrderItem. The $expand operator lets you specify this easily as part of your query. If I just wanted Orders, the query would look like this:

GET /zza/Customers?$expand=Orders

If I wanted to go deeper and get OrderItems, you use a / separator for each level in the hierarchy:

GET /zza/Customers?$expand=Orders/OrderItems

The arguments to the expand operator need to be the property name exposed on the entity type for the related object or collection. So to go one level deeper to the Product for each OrderItem, it would look like:

GET /zza/Customers?$expand=Orders/OrderItems/Product

However, if you tried to issue that query against an existing Web API CustomersController like the one showed earlier, you would get a 400 status code response with the following error:

The request includes a $expand path which is too deep. The maximum depth allowed is 2. To increase the limit, set the ‘MaxExpansionDepth‘ property on QueryableAttribute or ODataValidationSettings.

I wish more error messages were like this: tell me what is wrong and what to do about it. Basically they built in protection against clients being too greedy and asking to expand massive amounts of data in a single request. Simply adding a MaxExpansionDepth to the Queryable attribute on the Get method fixes this:

[Queryable(MaxExpansionDepth=3)]public override IQueryable Get(){    return _Context.Customers;}

For the .NET clients, WCF Data Services adds an Expand() extension method that lets you express the $expand query directly on the collection exposed from your WCF Data Services Client:

var expandQuery = _OdataProxy.Customers.Expand("Orders/OrderItems/Product");

 

Getting a single property value

Part of the OData query syntax includes being able to retrieve a single property value on some object with a query that looks like this:

GET /zza/Products(1)/Name

Out of the box, ASP.NET Web API OData does not handle requests with this format. However, all you need to do is add a method to the respective Controller (ProductsController in this case) for each property that you want to expose in this way:

public string GetName(int key){    var product = _Context.Products.FirstOrDefault(p => p.Id == key);    if (product == null)        throw ErrorsHelper.GetResourceNotFoundError(Request);    return product.Name;}

If you issue that request, you get back the property value in the payload formatted like this in JSON Light:

HTTP/1.1 200 OK...Content-Length: 97{  "odata.metadata":"http://localhost:39544/zza/$metadata#Edm.String","value":"Plain Cheese"}

However, sometimes you may just want just the value and not the surrounding structure. That is where the $value operator comes in that is now supported in Web API 2. Simply append /$value on the end of this query:

GET /zza/Products(1)/Name/$value Have the same method implemented to handle the call, and now the response payload will contain just the value returned with nothing else.

HTTP/1.1 200 OK...Content-Length: 12Plain Cheese

Supporting batch updates

The initial release of Web API OData supported CUD operations (Create, Update, Delete), but only if you issued a separate HTTP request for each entity with the appropriate verb (POST, PUT or PATCH, and DELETE respectively). If you have a rich client that allows multiple modifications to be made and cached on the client side, it is very inefficient to make a separate round trip for each entity that has modifications in terms of the time it takes to negotiate each request and its associated connection. The OData protocol supports sending a collection of changes in a construct called a change set. This is basically a multipart request message that contains a collection of individual request messages in its body. The server side is responsible for pulling out the individual requests from the message and executing them one at a time. Web API 2 added support for handling multipart messages in general, and thus can also use this for handling the change sets of OData.

When you send down a change set in OData, you are supposed to send it to the root collection address (i.e. /zza/Customers) with /$batch appended as an HTTP POST request. This instructs the server that the HTTP body should contain a changeset to parse and process for updates.

The default OData routing I showed earlier will throw an error if a request like this comes in. All you need to do in Web API 2 to start handling change sets is to add a batch handler to the routing:

public static void Register(HttpConfiguration config){    config.Routes.MapODataRoute("ZzaOData", "zza", GetEdmModel(),         batchHandler: new DefaultODataBatchHandler(            GlobalConfiguration.DefaultServer));}

Now you will be off and running handling OData change sets. If you are using the WCF Data Services Client from a .NET client, it issues individual requests per entity by default, but all you have to do to get it to switch to issuing a batch request when you call SaveChanges it pass it a parameter instructing it to do that:

var response = _OdataProxy.SaveChanges(SaveChangesOptions.Batch);

Now if you have made edits to multiple entities, added new ones, and deleted other ones on the client side, all those changes will be sent to the server in a single request.

So that wraps up our tour of what’s new for Web API OData in the latest release that came out with Visual Studio 2013 and .NET 4.5.1. All of these capabilities, including Web API 2 itself, all get added to your project through NuGet packages. So you don’t have to have .NET 4.5.1 to take advantage of them, they are backward compatible to .NET 4.0. If you add the ASP.NET Web API 2 OData NuGet package, it will make sure it has the Web API 2 NuGet packages installed as well.

As demonstrated, once you add the NuGet, $select and $expand operators just start working with no changes to your code. To get a single property value, you first add an accessor method for that property to the controller that takes in the entity key and has a naming convention of Get<PropertyName>(), returning the value. The OData routing will take care of dispatching calls to that method if they come in with the right addressing scheme. Now adding $value to the end of it gets you the raw value of the property in the response body. Finally, all you need to start supporting batch updates is to add a batchHandler parameter to your OData route, and take advantage of the built-in implementation DefaultODataBatchHandler.

You can download the complete sample code for this article here. If you want to dive deeper, check out Brian‘s courses Building ASP.NET Web API OData Services and Building Data-Centric Single Page Applications with Breeze. 

 

[转]ASP.NET web API 2 OData enhancements