Share via


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.