+
+
+
+
Software versions used in the tutorial
+
- Visual Studio 2013
- Web API 2
- OData Version 3
- Entity Framework 6
- Fiddler Web Debugging Proxy (Optional)
+
+
- Create the Visual Studio Project
- Add an Entity Model
- Add an OData Controller
- Add the EDM and Route
- Seed the Database (Optional)
- Exploring the OData Endpoint
- OData Serialization Formats
+
Create the Visual Studio Project
+
+
+
+
+
+
+
Add an Entity Model
+
+
+
+
+
Note
+
+
C#
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
+
+
+
Add an OData Controller
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Products.cs defines the Web API controller that implements the OData endpoint.
- ProductServiceContext.cs provides methods to query the underlying database, using Entity Framework.
+
+
Add the EDM and Route
+
C#
using ProductService.Models;
using System.Web.Http;
using System.Web.Http.OData.Builder;
namespace ProductService
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
}
}
}
+
- Creates an Entity Data Model (EDM) for the OData endpoint.
- Adds a route for the endpoint.
+
+
C#
modelBuilder.EntitySet<Product>("Products");
+
+
C#
config.Routes.MapODataRoute("ODataRoute", "odata", model);
+
+
Seed the Database (Optional)
+
+
console
Enable-Migrations
+
+
+
C#
protected override void Seed(ProductService.Models.ProductServiceContext context)
{
// New code
context.Products.AddOrUpdate(new Product[] {
new Product() { ID = 1, Name = "Hat", Price = 15, Category = "Apparel" },
new Product() { ID = 2, Name = "Socks", Price = 5, Category = "Apparel" },
new Product() { ID = 3, Name = "Scarf", Price = 12, Category = "Apparel" },
new Product() { ID = 4, Name = "Yo-yo", Price = 4.95M, Category = "Toys" },
new Product() { ID = 5, Name = "Puzzle", Price = 8, Category = "Toys" },
});
}
+
console
Add-Migration Initial
Update-Database
+
+
Exploring the OData Endpoint
+
+
+
Service Document
+
+
+
+
+
+
+
+
console
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/atomsvc+xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 17:51:01 GMT
Content-Length: 364
<?xml version="1.0" encoding="utf-8"?>
<service xml:base="http://localhost:60868/odata"
xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom">
<workspace>
<atom:title type="text">Default</atom:title>
<collection href="Products">
<atom:title type="text">Products</atom:title>
</collection>
</workspace>
</service></pre>
+
+
+
+
console
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 22:59:28 GMT
Content-Length: 136
{
"odata.metadata":"http://localhost:60868/odata/$metadata","value":[
{
"name":"Products","url":"Products"
}
]
}
Service Metadata Document
+
+
console
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:05:52 GMT
Content-Length: 1086
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
<edmx:DataServices m:DataServiceVersion="3.0" m:MaxDataServiceVersion="3.0"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<Schema Namespace="ProductService.Models" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityType Name="Product">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Name="ID" Type="Edm.Int32" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
<Property Name="Price" Type="Edm.Decimal" Nullable="false" />
<Property Name="Category" Type="Edm.String" />
</EntityType>
</Schema>
<Schema Namespace="Default" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityContainer Name="Container" m:IsDefaultEntityContainer="true">
<EntitySet Name="Products" EntityType="ProductService.Models.Product" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Entity Set
+
console
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:01:31 GMT
Content-Length: 459
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products","value":[
{
"ID":1,"Name":"Hat","Price":"15.00","Category":"Apparel"
},{
"ID":2,"Name":"Socks","Price":"5.00","Category":"Apparel"
},{
"ID":3,"Name":"Scarf","Price":"12.00","Category":"Apparel"
},{
"ID":4,"Name":"Yo-yo","Price":"4.95","Category":"Toys"
},{
"ID":5,"Name":"Puzzle","Price":"8.00","Category":"Toys"
}
]
}
Entity
+
console
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:04:29 GMT
Content-Length: 140
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products/@Element","ID":1,
"Name":"Hat","Price":"15.00","Category":"Apparel"
}
+
OData Serialization Formats
+
- Atom Pub (XML)
- JSON "light" (introduced in OData v3)
- JSON "verbose" (OData v2)
+
+
console
<?xml version="1.0" encoding="utf-8"?>
<entry xml:base="http://localhost:60868/odata" xmlns="http://www.w3.org/2005/Atom" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
<id>http://localhost:60868/odata/Products(1)</id>
<category term="ProductService.Models.Product" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<link rel="edit" href="http://localhost:60868/odata/Products(1)" />
<link rel="self" href="http://localhost:60868/odata/Products(1)" />
<title />
<updated>2013-09-23T23:42:11Z</updated>
<author>
<name />
</author>
<content type="application/xml">
<m:properties>
<d:ID m:type="Edm.Int32">1</d:ID>
<d:Name>Hat</d:Name>
<d:Price m:type="Edm.Decimal">15.00</d:Price>
<d:Category>Apparel</d:Category>
</m:properties>
</content>
</entry>
+
+
console
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products/@Element",
"ID":1,
"Name":"Hat",
"Price":"15.00",
"Category":"Apparel"
}
+
console
{
"d":{
"__metadata":{
"id":"http://localhost:18285/odata/Products(1)",
"uri":"http://localhost:18285/odata/Products(1)",
"type":"ProductService.Models.Product"
},"ID":1,"Name":"Hat","Price":"15.00","Category":"Apparel"
}
}
+
Next Steps
using Microsoft.Data.OData; using Microsoft.Data.OData.Query; using ProductService.Models; using System; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using System.Web.Http.ModelBinding; using System.Web.Http.OData; using System.Web.Http.OData.Routing; using System.Web.Http.Routing; namespace ProductService.Controllers { public class ProductsController : ODataController { private ProductServiceContext db = new ProductServiceContext(); // GET odata/Products [Queryable] public IQueryable<Product> GetProducts() { return db.Products; } // GET odata/Products(5) [Queryable] public SingleResult<Product> GetProduct([FromODataUri] int key) { return SingleResult.Create(db.Products.Where(product => product.ID == key)); } // PUT odata/Products(5) public async Task<IHttpActionResult> Put([FromODataUri] int key, Product product) { if (!ModelState.IsValid) { return BadRequest(ModelState); } if (key != product.ID) { return BadRequest(); } db.Entry(product).State = EntityState.Modified; try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!ProductExists(key)) { return NotFound(); } else { throw; } } return Updated(product); } // POST odata/Products public async Task<IHttpActionResult> Post(Product product) { if (!ModelState.IsValid) { return BadRequest(ModelState); } db.Products.Add(product); await db.SaveChangesAsync(); return Created(product); } // PATCH odata/Products(5) [AcceptVerbs("PATCH", "MERGE")] public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> patch) { if (!ModelState.IsValid) { return BadRequest(ModelState); } Product product = await db.Products.FindAsync(key); if (product == null) { return NotFound(); } patch.Patch(product); try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!ProductExists(key)) { return NotFound(); } else { throw; } } return Updated(product); } // DELETE odata/Products(5) public async Task<IHttpActionResult> Delete([FromODataUri] int key) { Product product = await db.Products.FindAsync(key); if (product == null) { return NotFound(); } db.Products.Remove(product); await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent); } // GET /Products(1)/Supplier public Supplier GetSupplier([FromODataUri] int key) { Product product = db.Products.FirstOrDefault(p => p.ID == key); if (product == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return product.Supplier; } protected override void Dispose(bool disposing) { if (disposing) { db.Dispose(); } base.Dispose(disposing); } private bool ProductExists(int key) { return db.Products.Count(e => e.ID == key) > 0; } // The next methods support entity relations. // For more information, see http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/working-with-entity-relations public async Task<IHttpActionResult> CreateLink([FromODataUri] int key, string navigationProperty, [FromBody] Uri link) { if (!ModelState.IsValid) { return BadRequest(ModelState); } Product product = await db.Products.FindAsync(key); if (product == null) { return NotFound(); } switch (navigationProperty) { case "Supplier": string supplierKey = GetKeyFromLinkUri<string>(link); Supplier supplier = await db.Suppliers.FindAsync(supplierKey); if (supplier == null) { return NotFound(); } product.Supplier = supplier; await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent); default: return NotFound(); } } public async Task<IHttpActionResult> DeleteLink([FromODataUri] int key, string navigationProperty) { Product product = await db.Products.FindAsync(key); if (product == null) { return NotFound(); } switch (navigationProperty) { case "Supplier": product.Supplier = null; await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent); default: return NotFound(); } } // Helper method to extract the key from an OData link URI. private TKey GetKeyFromLinkUri<TKey>(Uri link) { TKey key = default(TKey); // Get the route that was used for this request. IHttpRoute route = Request.GetRouteData().Route; // Create an equivalent self-hosted route. IHttpRoute newRoute = new HttpRoute(route.RouteTemplate, new HttpRouteValueDictionary(route.Defaults), new HttpRouteValueDictionary(route.Constraints), new HttpRouteValueDictionary(route.DataTokens), route.Handler); // Create a fake GET request for the link URI. var tmpRequest = new HttpRequestMessage(HttpMethod.Get, link); // Send this request through the routing process. var routeData = newRoute.GetRouteData( Request.GetConfiguration().VirtualPathRoot, tmpRequest); // If the GET request matches the route, use the path segments to find the key. if (routeData != null) { ODataPath path = tmpRequest.GetODataPath(); var segment = path.Segments.OfType<KeyValuePathSegment>().FirstOrDefault(); if (segment != null) { // Convert the segment into the key type. key = (TKey)ODataUriUtils.ConvertFromUriLiteral( segment.Value, ODataVersion.V3); } } return key; } // The next method implements an OData action. // For more information, see http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-actions // POST /odata/Products(5)/RateProduct [HttpPost] public async Task<IHttpActionResult> RateProduct([FromODataUri] int key, ODataActionParameters parameters) { if (!ModelState.IsValid) { return BadRequest(); } int rating = (int)parameters["Rating"]; Product product = await db.Products.FindAsync(key); if (product == null) { return NotFound(); } product.Ratings.Add(new ProductRating() { Rating = rating }); db.SaveChanges(); double average = product.Ratings.Average(x => x.Rating); return Ok(average); } } }