Search code examples
.net-coreodata

Missing OData envelope/context


When I request some data through OData I will get the data as requested, but not the OData envelope/context. This means that any OData functionality wont work for the client.

I am trying to understand the reason why I do not get the OData envelope together with the data.

The setup is as follows:

  • .Net Core
  • OData
  • PostgreSQL

I am requesting data with this url: http://localhost:5000/odata/mycontroller/mydbview.

The controller class:

public class MyController : ODataController
{
    [EnableQuery]
    [ODataRoute("MyController/MyDBView")]
    public IActionResult GetSeasonView(int id)
    {
        var requestedItems = myTableContext.MyDBView;

        return Ok(requestedItems);
    }
}

The EDM model:

public static IEdmModel GetEdmModel()
{
    var odataModelBuilder = new ODataConventionModelBuilder();
    odataModelBuilder.EntitySet<MyDataDto>("My");

    return odataModelBuilder.GetEdmModel();
}

The data object looks like:

[Table("datatable", Schema = "api")]
public class MyDataDto
{
    [Key] 
    public string Id { get; set; }
    public string Status { get; set; }
    public uint Total { get; set; }
  
    <Continues with more data>

The view, which is a subset of the data table, looks like:

[Table("datatable", Schema = "api")]
public class MyDBView
{
    [Key]
    public string id { get; set; }
    public string status { get; set; }
    public uint total { get; set; }
}

Any sugestions to what could be wrong with this setup? I have tried to follow this guide btw: https://www.laboremus.ug/post/using-sql-views-with-entity-framework-code-first

Update

I have found the problem but still not a solution. The problem is that the entity set is registered as a MyDataDTO while the return value is a MyDBView.

I still don't know the solution though.


Solution

  • There are a few issues, your intended route does not conform to OData which makes it hard to interpret your expected usage, is this a collection, or item based request? For instance you have an 'id' parameter that is not used within the method, and the route prefix MyController doesn't match the controller registration through the route My.

    For this solution I can demonstrate the configuration for both scenarios:

    • A Collection bound function to return the entire view
    • An Item bound function to return the view scoped to a given item Id

    You need to register the View DTO with the OData model if you want to use the IActionResult as the response type on your endpoint method, alternatively can return IQueryable<MyDbView>.

    • this can work because now the attribute routes can identify the intended type to return
    • but IActionResult gives you the flexibility to return error responses or other standard forms of the response.

    Lets look at the 2 methods for this view, one returns the entire view and will support $filter query options, the other returns a record from the view for an individual item:

    NOTE: I have changed the names of the methods from your example to reduce confusion with the configuration

    public class MyController : ODataController
    {
        // example route: ~/My/DBView
        [EnableQuery]
        [HttpGet]
        public IActionResult DBView()
        {
            var requestedItems = myTableContext.MyDBView;
            return Ok(requestedItems);
        }
    
        // example route: ~/My(id)/asDBView
        [EnableQuery]
        [HttpGet]
        public IActionResult AsDBView(int key)
        {
            var requestedItem = myTableContext.MyDBView.Where(x => x.Id == key);
            return Ok(requestedItems);
        }
    }
    

    Now we care register these routes with the OData model through the IEdmBuilder. For the view to support querying, you will need to specify conceptual name of the dataset, but this doesn't need to correspond to an actual controller.

    public static IEdmModel GetEdmModel()
    {
        var odataModelBuilder = new ODataConventionModelBuilder();
        // Initialize your OData EntitySets
        odataModelBuilder.EntitySet<MyDataDto>("My");
    
        // initialize the Custom DTOs
        odataModelBuilder.EntitySet<MyDbView>("MyDbViewFakeSet"); 
        // NOTE: Name is arbitrary, but MUST be unique across all EntitySets.
    
        // Register the Function endpoints on your "My" controller
        odataModelBuilder.EntitySet<MyDataDto>("My")
                         .Collection.Function("DBView")
                         .ReturnsCollectionFromEntitySet<MyDbView>("MyDbViewFakeSet");
    
        odataModelBuilder.EntitySet<MyDataDto>("My")
                         .EntityType.Function("asDBView")
                         .ReturnsFromEntitySet<MyDbView>("MyDbViewFakeSet");
    
    
        return odataModelBuilder.GetEdmModel();
    }