Open Types in OData v4 with ASP.NET Web API
by Microsoft
In OData v4, an open type is a structured type that contains dynamic properties, in addition to any properties that are declared in the type definition. Open types let you add flexibility to your data models. This tutorial shows how to use open types in ASP.NET Web API OData.
This tutorial assumes that you already know how to create an OData endpoint in ASP.NET Web API. If not, start by reading Create an OData v4 Endpoint first.
Software versions used in the tutorial
- Web API OData 5.3
- OData v4
First, some OData terminology:
- Entity type: A structured type with a key.
- Complex type: A structured type without a key.
- Open type: A type with dynamic properties. Both entity types and complex types can be open.
The value of a dynamic property can be a primitive type, complex type, or enumeration type; or a collection of any of those types. For more information about open types, see the OData v4 specification.
Install the Web OData Libraries
Use NuGet Package Manager to install the latest Web API OData libraries. From the Package Manager Console window:
Install-Package Microsoft.AspNet.OData
Install-Package Microsoft.AspNet.WebApi.OData
Define the CLR Types
Start by defining the EDM models as CLR types.
public enum Category
{
Book,
Magazine,
EBook
}
public class Address
{
public string City { get; set; }
public string Street { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Press
{
public string Name { get; set; }
public string Email { get; set; }
public Category Category { get; set; }
public IDictionary<string, object> DynamicProperties { get; set; }
}
public class Book
{
[Key]
public string ISBN { get; set; }
public string Title { get; set; }
public Press Press { get; set; }
public IDictionary<string, object> Properties { get; set; }
}
When the Entity Data Model (EDM) is created,
Category
is an enumeration type.Address
is a complex type. (It does not have a key, so it is not an entity type.)Customer
is an entity type. (It has a key.)Press
is an open complex type.Book
is an open entity type.
To create an open type, the CLR type must have a property of type IDictionary<string, object>
, which holds the dynamic properties.
Build the EDM Model
If you use ODataConventionModelBuilder to create the EDM, Press
and Book
are automatically added as open types, based on the presence of a IDictionary<string, object>
property.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Book>("Books");
builder.EntitySet<Customer>("Customers");
var model = builder.GetEdmModel();
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
model: model);
}
}
You can also build the EDM explicitly, using ODataModelBuilder.
ODataModelBuilder builder = new ODataModelBuilder();
ComplexTypeConfiguration<Press> pressType = builder.ComplexType<Press>();
pressType.Property(c => c.Name);
// ...
pressType.HasDynamicProperties(c => c.DynamicProperties);
EntityTypeConfiguration<Book> bookType = builder.EntityType<Book>();
bookType.HasKey(c => c.ISBN);
bookType.Property(c => c.Title);
// ...
bookType.ComplexProperty(c => c.Press);
bookType.HasDynamicProperties(c => c.Properties);
// ...
builder.EntitySet<Book>("Books");
IEdmModel model = builder.GetEdmModel();
Add an OData Controller
Next, add an OData controller. For this tutorial, we'll use a simplified controller that just supports GET and POST requests, and uses an in-memory list to store entities.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Web.OData;
namespace MyApp.Controllers
{
public class BooksController : ODataController
{
private IList<Book> _books = new List<Book>
{
new Book
{
ISBN = "978-0-7356-8383-9",
Title = "SignalR Programming in Microsoft ASP.NET",
Press = new Press
{
Name = "Microsoft Press",
Category = Category.Book
}
},
new Book
{
ISBN = "978-0-7356-7942-9",
Title = "Microsoft Azure SQL Database Step by Step",
Press = new Press
{
Name = "Microsoft Press",
Category = Category.EBook,
DynamicProperties = new Dictionary<string, object>
{
{ "Blog", "https://blogs.msdn.com/b/microsoft_press/" },
{ "Address", new Address {
City = "Redmond", Street = "One Microsoft Way" }
}
}
},
Properties = new Dictionary<string, object>
{
{ "Published", new DateTimeOffset(2014, 7, 3, 0, 0, 0, 0, new TimeSpan(0))},
{ "Authors", new [] { "Leonard G. Lobel", "Eric D. Boyd" }},
{ "OtherCategories", new [] {Category.Book, Category.Magazine}}
}
}
};
[EnableQuery]
public IQueryable<Book> Get()
{
return _books.AsQueryable();
}
public IHttpActionResult Get([FromODataUri]string key)
{
Book book = _books.FirstOrDefault(e => e.ISBN == key);
if (book == null)
{
return NotFound();
}
return Ok(book);
}
public IHttpActionResult Post(Book book)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// For this sample, we aren't enforcing unique keys.
_books.Add(book);
return Created(book);
}
}
}
Notice that the first Book
instance has no dynamic properties. The second Book
instance has the following dynamic properties:
- "Published": Primitive type
- "Authors": Collection of primitive types
- "OtherCategories": Collection of enumeration types.
Also, the Press
property of that Book
instance has the following dynamic properties:
- "Blog": Primitive type
- "Address": Complex type
Query the Metadata
To get the OData metadata document, send a GET request to ~/$metadata
. The response body should look similar to this:
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="MyApp.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Book" OpenType="true">
<Key>
<PropertyRef Name="ISBN" />
</Key>
<Property Name="ISBN" Type="Edm.String" Nullable="false" />
<Property Name="Title" Type="Edm.String" />
<Property Name="Press" Type="MyApp.Models.Press" />
</EntityType>
<EntityType Name="Customer">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
<Property Name="Address" Type="MyApp.Models.Address" />
</EntityType>
<ComplexType Name="Press" OpenType="true">
<Property Name="Name" Type="Edm.String" />
<Property Name="Category" Type="MyApp.Models.Category" Nullable="false" />
</ComplexType>
<ComplexType Name="Address">
<Property Name="City" Type="Edm.String" />
<Property Name="Street" Type="Edm.String" />
</ComplexType>
<EnumType Name="Category">
<Member Name="Book" Value="0" />
<Member Name="Magazine" Value="1" />
<Member Name="EBook" Value="2" />
</EnumType>
</Schema>
<Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityContainer Name="Container">
<EntitySet Name="Books" EntityType="MyApp.Models.Book" />
<EntitySet Name="Customers" EntityType="MyApp.Models.Customer" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
From the metadata document, you can see that:
- For the
Book
andPress
types, the value of theOpenType
attribute is true. TheCustomer
andAddress
types don't have this attribute. - The
Book
entity type has three declared properties: ISBN, Title, and Press. The OData metadata does not include theBook.Properties
property from the CLR class. - Similarly, the
Press
complex type has only two declared properties: Name and Category. The metadata does not include thePress.DynamicProperties
property from the CLR class.
Query an Entity
To get the book with ISBN equal to "978-0-7356-7942-9", send a GET request to ~/Books('978-0-7356-7942-9')
. The response body should look similar to the following. (Indented to make it more readable.)
{
"@odata.context":"http://localhost:37141/$metadata#Books/$entity",
"ISBN":"978-0-7356-7942-9",
"Title":"Microsoft Azure SQL Database Step by Step",
"Press":{
"Name":"Microsoft Press",
"Category":"EBook",
"Blog":"https://blogs.msdn.com/b/microsoft_press/",
"Address":{
"@odata.type":"#MyApp.Models.Address",
"City":"Redmond",
"Street":"One Microsoft Way"
}
},
"Published":"2014-07-03T00:00:00Z",
"Authors@odata.type":"#Collection(String)",
"Authors":[
"Leonard G. Lobel","Eric D. Boyd"
],
"OtherCategories@odata.type":"#Collection(MyApp.Models.Category)",
"OtherCategories":[
"Book","Magazine"
]
}
Notice that the dynamic properties are included inline with the declared properties.
POST an Entity
To add a Book entity, send a POST request to ~/Books
. The client can set dynamic properties in the request payload.
Here is an example request. Note the "Price" and "Published" properties.
POST http://localhost:37141/Books HTTP/1.1
User-Agent: Fiddler
Host: localhost:37141
Content-Type: application/json
Content-Length: 191
{
"ISBN":"978-0-7356-8383-9","Title":"Programming Microsoft ASP.NET MVC","Press":{
"Name":"Microsoft Press","Category":"Book"
}, "Price": 49.99, "Published":"2014-02-15T00:00:00Z"
}
If you set a breakpoint in the controller method, you can see that Web API added these properties to the Properties
dictionary.