Search code examples
entity-frameworkodataasp.net-web-api2data-modelingodatacontroller

How to create custom entity for Web API 2 OData Data controller


I have the need to migrate my traditional Web API 2 Data controllers over to OData v4 style data controllers. This works very easily with the standard one-to-one table-to-entity relationship but I now have the need to use several different tables (which have no real constraints) into my data controller response. I am having trouble figuring out how to register this new custom "entity" in my WebAPI.config file.

Here is an example of what my WebAPIconfig.cs looks like:

using System.Linq;
using System.Web.Http;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
using System.Web.OData.Routing;
using System.Web.OData.Routing.Conventions;
using MyProject.Models;

namespace MyProject
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and routes
            config.MapHttpAttributeRoutes();

            //OData configuration
            ODataModelBuilder builder = new ODataConventionModelBuilder();
            builder.EntitySet<Order>("orders");
            builder.EntitySet<Customer>("customers");

            //what goes here for my "custom" entity?

            var _model = builder.GetEdmModel();
            var defaultConventions = ODataRoutingConventions.CreateDefaultWithAttributeRouting(config, _model);

            //var defaultConventions = ODataRoutingConventions.CreateDefault();
            var conventions = defaultConventions.Except(
                    defaultConventions.OfType<MetadataRoutingConvention>());

            config.MapODataServiceRoute(
                routeName: "ODataRoute",
                routePrefix: "api",
                routingConventions: conventions,
                pathHandler: new DefaultODataPathHandler(),
                model: _model);         

            //ensure JSON responses
            var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
            config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
        }
    }
}

Here is an example of what my Web API 2 data controller looks like:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using MyProject.Models;

namespace MyProject.DataControllers
{
    public class OrderDetailsController : ApiController
    {
        public OrderDetails GetOrderDetails(int id)
        {
            var ctx = new MyDatabaseEntities();
            var details = new OrderDetails();
            var order = ctx.Orders.FirstOrDefault(o => o.orderID == id);
            if (order == null)
            {
                return details; //return an empty details object to the UI and abandon this code
            }

            //Data objects necessary for the order details page
            IEnumerable<orderCertification> coupons = ctx.Coupons;
            var importances     = ctx.OrderImportances.Where(x => x.ImportanceId == order.ImportanceId).Where(x => (x.Type == "IMPORTANCES")) ?? null;
            var rankings        = ctx.OrderImportances.Where(x => x.ImportanceId == order.ImportanceId).Where(x => (x.Type == "RANK")) ?? null;
            var profits         = ctx.OrderImportances.Where(x => x.ImportanceId == order.ImportanceId).Where(x => (x.Type == "PROFIT")) ?? null;
            var address         = ctx.CustomerAddress.Where(c => c.OrderId == order.Id) ?? null;
            var email           = ctx.CustomerEmail.Where(c => c.Id == order.Id).Where(x => x.Type == "EMAIL") ?? null;         
            var giftcards       = ctx.GiftCardAssignments.Where(c => c.CardID == order.CardID).FirstOrDefault().ToList() ?? null;
            var customerCoupons = coupons.Where(c => giftCards.Any(o => o.GiftCardID == c.Id)).OrderBy(c => c.CouponName) ?? null;

            //lots of other fun and crazy properties get set here!! etc etc.

            //Set the order details properties 
            details.OrderImportances    = importances;
            details.OrderRankings       = rankings;
            details.OrderProfits        = profits;
            details.OrderAddress        = address;
            details.OrderEmail          = email;
            details.OrderGiftCards      = giftcards;
            details.OrderCoupons        = customerCoupons;
            details.OrderDescription    = "This is my order description string.";

            return details;
        }
    }
}

And here is an example of what my OrderDetails() class currently looks like:

using System.Collections.Generic;

namespace MyProject.Models
{
    public class OrderDetails
    {

        public IEnumerable<OrderImportance> OrderImportances { get; set; }
        public IEnumerable<OrderImportance> OrderRankings { get; set; }
        public IEnumerable<OrderImportance> OrderProfits { get; set; }
        public string OrderAddress { get; set; }
        public string OrderEmail { get; set; }
        public IEnumerable<OrderGiftCard> OrderGiftCards { get; set; }
        public IEnumerable<OrderCoupon> OrderCoupons { get; set; }
        public string OrderDescription { get; set; }

    }
}

How do I make the OData version of this Web API controller and how do I register my OrderDetails class for it, in my WebAPIConfig.cs?


Solution

  • OrderDetails does not appear to have a key property. Therefore, it is not an entity type (a named structured type with a key), but rather a complex type (a keyless named structured type consisting of a set of properties).

    Since complex types do not have their own identity (i.e., key), they cannot be exposed as entity sets in an OData service. This means you will not configure OrderDetails in the model builder, nor will you create a separate controller for OrderDetails.

    The most straightforward way to migrate your existing GetOrderDetails method to OData is to redefine it as an OData function bound to the orders entity set. Actions and Functions in OData v4 Using ASP.NET Web API 2.2 provides a good tutorial on defining and configuring OData functions, but here's the gist of what you need to do.

    Declare the function in WebApiConfig.Register:

    builder.EntityType<Order>().Function("GetOrderDetails").Returns<OrderDetails>();
    

    Define the function in the controller for the orders entity set:

    public class OrdersController : ODataController
    {
        // Other methods for GET, POST, etc., go here.
    
        [HttpGet]
        public OrderDetails GetOrderDetails([FromODataUri] int key)
        {
            // Your application logic goes here.
        }
    }
    

    Finally, invoke the function as follows:

    GET http://host/api/orders(123)/Default.GetOrderDetails
    

    Note that Default is the default namespace of the service, which is normally required when invoking a bound function. To change this, you can either set builder.Namespace, or you can enable unqualified function calls with config.EnableUnqualifiedNameCall(true).