Typeless Entity Object Support in WebApi

The Problem

In current WebApi impl you will be using an ODataMobuilder or ODataConventionModelBuilder to setup your model for oData feed. ODataConventionModelBuilder is the preferred way as it’d automatically map CLR classes to an EDM model based on a set of predefined Conventions.

For example you have class product and will be writing below code to setup your product model: 

 public class Product
{
        public int ID { get; set; }

        public string Name { get; set; }

        public DateTime? ReleaseDate { get; set; }

        public DateTime? SupportedUntil { get; set; }

        public virtual ProductFamily Family { get; set; }
}
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");  

 

The problem with both current model builder that is that it requires a strong CLR type (Product in above code snippet) for each and every EDM type that the service is exposing. The mapping between the CLR type and the EDM type is one-to-one and not configurable.

To address that we introduced a set of new interfaces and classes that will be discussed below to support building entity object without backing CLR type. This is quite handy in the scenarios where the model is built in runtime. For example your data model will be matching a database schedule that is only discoverable when you load it, and all the payload is from the tables in that database.

Typeless Impl Internals

There are 4 key interfaces defined to enable the typeless feature. IEdmObject, IEdmStructuredObject, IEdmEntityObject and IEdmComplexObject. See below diagram for their inheritance relationship.

clip_image002

 

 public interface IEdmObject
{
        IEdmTypeReference GetEdmType();
}
public interface IEdmStructuredObject : IEdmObject
{
        bool TryGetPropertyValue(string propertyName, out object value);
}

The classes EdmEntityObject, EdmComplexObject, EdmEntityCollectionObject, EdmComplexCollectionObject are created to impl above interfaces to model a typeless object system. 
public class EdmEntityObject : EdmStructuredObject, IEdmEntityObject
{… …}

public abstract class EdmStructuredObject : Delta, IEdmStructuredObject
{… …}

 

(Delta inheritance enables the EdmEntityObject the ability to track a set of changes for an entity object, you will be using that in a patch scenario)

Let’s take a look at the default implementation of the function TryGetPropertyValue/TrySetPropertyValue for EdmEntityObject and EdmStructuredObject

 public override bool TryGetPropertyValue(string name, out object value)
{
    IEdmProperty property = _actualEdmType.FindProperty(name);
    if (property != null)
    {
        if (_container.ContainsKey(name))
        {
            value = _container[name];
            return true;
        }
        else
        {
            value = GetDefaultValue(property.Type);
            // store the default value (but don't update the list of 'set properties').
            _container[name] = value;
            return true;
        }
    }
    else
    {
        value = null;
        return false;
    }
}

 The code itself is pretty straightforward in that

1) It uses actualEdmType to record the type info for a property.

2) A Dictionary container is used to record the actual property value.

3) The class also provides many other helper functions, for example to locate a property by type and etc.

Lets create an example to learn how to setup and use a model without strong CLR type in ASP.NET Web API. To put the example as simple as possible. Let’s assume:

1) You already loaded the scheme from any sql connection and knew type info of the column properties.

2) There are only 2 associated tables in the DB. Product table and category table in a one to many relationship, which means a product can only have 0 to 1 category and a category can have multiple products in it.

 Create the Model

First we need to setup the EDM model for product and category and their associations.

 
        private static IEdmModel GetEdmModel()
        {
            EdmModel model = new EdmModel();

            // create & add entity type to the model.
            EdmEntityType product = new EdmEntityType("Org.Microsoft", "Product");
            product.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String);
            product.AddStructuralProperty("Price", EdmPrimitiveTypeKind.Double);
            var key = product.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32);
            product.AddKeys(key);
            model.AddElement(product);

            EdmEntityType category = new EdmEntityType("Org.Microsoft", "Category");
            category.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String);
            var key1 = category.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32);
            category.AddKeys(key1);
            model.AddElement(category);

            // setup navigations

            EdmNavigationPropertyInfo infoFrom = new EdmNavigationPropertyInfo();
            infoFrom.Name = "Product";
            infoFrom.TargetMultiplicity = EdmMultiplicity.Many;
            infoFrom.Target = product as IEdmEntityType;

            EdmNavigationPropertyInfo infoTo = new EdmNavigationPropertyInfo();
            infoFrom.Name = "Category";
            infoFrom.TargetMultiplicity = EdmMultiplicity.One;
            infoFrom.Target = category as IEdmEntityType;

            EdmNavigationProperty productCategory = product.AddBidirectionalNavigation(infoFrom, infoTo);

            // create & add entity container to the model.
            EdmEntityContainer container = new EdmEntityContainer("Org.Microsoft", "ProductsService");
            model.AddElement(container);
            model.SetIsDefaultEntityContainer(container, isDefaultContainer: true); // set this as the default container for smaller json light links.

            // create & add entity set 'Products'.
            EdmEntitySet Products = container.AddEntitySet("Products", product);
            EdmEntitySet Categories = container.AddEntitySet("Caegories", category);
            Products.AddNavigationTarget(productCategory, Categories);
            return model;
        }

 

Add a Simple Controller

Again for simplicity reason let’s build a controller that maps every request to a SimpleController.

 public string SelectController(ODataPath odataPath, HttpRequestMessage request)
{
    Console.WriteLine(odataPath.PathTemplate);
    ODataPathSegment firstSegment = odataPath.Segments.FirstOrDefault();

    if (firstSegment != null && firstSegment is EntitySetPathSegment)
    {
        return "Simple";
    }

    return null;
}

 

Response Handler

Lets built 3 handlers to impl 3 scenarios

1) Get product by ID. By sending GET request https://localhost/odata/Products

 public EdmEntityObjectCollection Get()
{
            ODataPath path = Request.GetODataPath();
            IEdmType edmType = path.EdmType;
            Contract.Assert(edmType.TypeKind == EdmTypeKind.Collection, "we are serving get {entityset}");

            IEdmEntityTypeReference entityType = (edmType as IEdmCollectionType).ElementType.AsEntity();

            var collectionProduct = new EdmEntityObjectCollection(new EdmCollectionTypeReference(edmType as IEdmCollectionType, isNullable: false));

            var entityProduct = new EdmEntityObject(entityType);
            entityProduct.TrySetPropertyValue("Name", "Microsoft Windows");
            entityProduct.TrySetPropertyValue("Price", 99.99);
            entityProduct.TrySetPropertyValue("ID", 12345);

            var entityCategory = new EdmEntityObject(entityType);
            entityCategory.TrySetPropertyValue("Name", "Category 1");
            entityCategory.TrySetPropertyValue("ID", 10000);

            entityProduct.TrySetPropertyValue("Category", entityCategory);
            collectionProduct.Add(entityProduct);
            return collectionProduct;
}

 

2) Get the category of given product by sending GET request https://localhost/odata/Products(12345)/Category

 public IEdmEntityObject GetCategory(string key)
{
    ODataPath path = Request.GetODataPath();
    IEdmEntityType entityType = path.EdmType as IEdmEntityType;

    var entity = new EdmEntityObject(entityType);
    entity.TrySetPropertyValue("Name", "Category 1");
    entity.TrySetPropertyValue("ID", 10000);
    return entity;
}

The key to compose the payload is to create EdmEntityObject and populate the properties by using TrySetPropertyValue() interface.

3) Post a new product entity to the control by sending

On the client side we do:

 var request = new HttpRequestMessage(HttpMethod.Post, "https://localhost/odata/Products");

 request.Content = new StringContent("{ 'Name': 'leo', Price : 1.99, ID: 12345}", Encoding.Default, "application/json");
PrintResponse(client.SendAsync(request).Result);
 In the post handler we do to deserialize an IdmEntityObject
 public HttpResponseMessage Post(IEdmEntityObject entity)
{
    ODataPath path = Request.GetODataPath();
    IEdmType edmType = path.EdmType;
    Contract.Assert(edmType.TypeKind == EdmTypeKind.Collection, "we are serving POST {entityset}");

    IEdmEntityTypeReference entityType = (edmType as IEdmCollectionType).ElementType.AsEntity();

    // do something with the entity object here.

    return Request.CreateResponse(HttpStatusCode.Created, entity);
 }
  

Summary

The new feature of the IEdmEntityObject that ships in oData version 5.0 adds support for creating an entity model object without strong CLR type backing. This provides better integration ability with your existing data model regardless your data type and source.

Comments

  • Anonymous
    November 12, 2013
    Hi Leo, thanks for the excellent tutorial, hope to see more on typeless OData support in WebAPI in the future :) One question though: is is possible to still use the ODataQueryOptions parameter in the actions? Ive tried it but im getting errors: "The given model does not contain the type 'System.Web.Http.OData.IEdmEntityObject'". Passing in a generic type like ODataQueryOptions<IEdmModel> wont work either...

  • Anonymous
    November 13, 2013
    You should build ODataQueryOptions directly in your controller using the EDM type (element type) that your query would operate on.            // build the typeless query options using the edm type.            ODataQueryContext queryContext = new ODataQueryContext(Request.GetEdmModel(), entityType);            ODataQueryOptions queryOptions = new ODataQueryOptions(queryContext, Request);

  • Anonymous
    November 15, 2013
    Hi Leo, thanks for this example. Seams to be exactly what I need. Could you offer this example as download? Could be helpfull for the first tests.

  • Anonymous
    November 16, 2013
    Hi Leo, I could test it and it's excellent. I'm using Excel to read this data. Do you know, how I can enable paging to receive top-value and reduce memory usage of one single request for all data? Can I let creating help pages with this properties of my data like in web.api?

  • Anonymous
    December 03, 2013
    Thanks for the sample...but The sample is very hard to understand for example not clear what to do here: "Add a Simple Controller" Not clear how to set that odata xml will be return instead of json

  • Anonymous
    December 05, 2013
    Hi Leo With your example, I could create a part of the OData web api. My current problem are the relationships between the data. With your example, could Excel read this relationship between product and category and show it without user interaction?

  • Anonymous
    January 12, 2014
    To make things more clear to everyone about "Add a Simple Controller" At this point author implements IODataRoutingConvention interface or derives from EntitySetRoutingConvention class to route all odata requests to single controller. More information about custom OData routing conventions see here: www.asp.net/.../odata-routing-conventions

  • Anonymous
    January 24, 2014
    Leo, I have followed your example and have typeless working except for navigation properties with select and expand option.  How do I  configure/ program for a single valued navigation property and query specifying $expand=Person that needs to return a null where they odata json body should contain : Person=(null)? My problem is not with quireyOptions.request.SetSelectExpandClause, that's working perfectly.  My problem is what to return from TryGetPropertyValue for the null single valued navigation property.  Any references you can point to would be greatly appreciated.

  • Anonymous
    February 22, 2014
    Hi, Can you please share the complete code. Its very difficult to make out from the code snippets above. Thanks, Ks

  • Anonymous
    March 08, 2014
    Hi Leo! Would you please give me a link to download your example?!! Thank you very much

  • Anonymous
    March 09, 2014
    to all, we are release webapi 5.2  internal preview version this week. you should be able to find this sample on asp.net website. for any question, please drop a mail to my mailbox. leohu@microsoft.com

  • Anonymous
    March 24, 2014
    Hi @LeoHu1, Could you post a link to this please Thanks

  • Anonymous
    May 22, 2014
    I can't seem to get this example to work, the error I get is: Type 'System.Web.Http.OData.EdmEntityObject' cannot be serialized. Anyone else have this problem? Do I need to define a custom serializer/deserializer?

  • Anonymous
    May 25, 2014
    I'm having the same error as you Cory.

  • Anonymous
    May 26, 2014
    How can I apply the query options. When I'm using the .ApplyTo method i'm getting this error. The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type.

  • Anonymous
    June 02, 2014
    If you get a serialization exception in the controller, please check that your Products web api controller class inherits from ODataController and not just ApiController.

  • Anonymous
    July 09, 2014
    Arrows in the inheritance diagram should point the other way.

  • Anonymous
    March 03, 2015
    For anyone looking for the direct link on the sample solution on this: aspnet.codeplex.com/.../latest