2

IN a OData v4 Controller, is it possible to return different models for the Get() and the Get([FromIDataUri] key)?

I like to use ViewModels, and when using the Get() method I'd like to return an xxxOverviewViewModel. When using the Get([FromIDataUri] key) method, I'd like to return a xxxViewModel.

Is this possible, and if so, how?

I've tried to return different models, but I always get a 406 Acceptable.

Webapi.config:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.EnableCors();

        config.MapODataServiceRoute("ODataRoute", "odata", GetEdmModel());

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Filter().Expand().Select().OrderBy().MaxTop(null).Count();
    }

    private static IEdmModel GetEdmModel()
    {
        var builder = new ODataConventionModelBuilder();
        builder.EntitySet<ComplaintViewModel>("ComplaintOData");
        return builder.GetEdmModel();

    }
}

ComplaintODataController

public class ComplaintODataController : ODataController
{
    private readonly QueryProcessor _queryProcessor;

    public ComplaintODataController(QueryProcessor queryProcessor)
    {
        _queryProcessor = queryProcessor;
    }

    [EnableQuery]
    public IQueryable<ComplaintOverviewViewModel> Get()
    {
        var result = _queryProcessor.Handle(new GetAllComplaintsQuery());
        return result;
    }

    // WHEN CALLING THIS METHOD I GET A 406: 
    [EnableQuery]
    public ComplaintViewModel Get([FromODataUri] int key)
    {
        var result = _queryProcessor.Handle(new GetComplaintByIdQuery { Id = key });
        return result;
    }
}

EDIT:

My GetAllComplaintsQuery.Handle method looks like this:

public IQueryable<ComplaintOverviewViewModel> Handle(GetAllComplaintsQuery query)
{
    // .All is an IQueryable<Complaint>
    var result = _unitOfWork.Complaints.All.Select(c => new ComplaintOverviewViewModel
    {
        ComplaintType = c.ComplaintType.Description,
        CreationDate = c.CreationDate,
        Customer = c.Customer,
        Description = c.Description,
        Id = c.Id,
        SequenceNumber = c.SequenceNumber,
        Creator = c.Creator.Name
    });

    return result;
}

And this is my ComplaintConfiguration

public class ComplaintConfiguration : EntityTypeConfiguration<Complaint>
{
    public ComplaintConfiguration()
    {
        Property(p => p.SequenceNumber).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
        Property(p => p.Description).IsRequired();
    }
}
Martijn
  • 24,441
  • 60
  • 174
  • 261

3 Answers3

1

You did not include definitions for your ViewModel classes, so let me first make sure we're on the same page regarding what you're actually trying to achieve.

It looks like you want to return some restricted set of fields when client requests a list of Complaint records from the parameters-less Get() but when the client requests a specific complaint from Get([FromODataUri] int key) method you want to include additional fields.

I modeled this assumption using the following hierarchy:

public class ComplaintTypeViewModel
{
    public int Id { get; set; }
}

public class ComplaintOverviewViewModel : ComplaintTypeViewModel
{
    public string Name { get; set; }
}

public class ComplaintViewModel : ComplaintOverviewViewModel
{
    public string Details { get; set; }
}

Ran tests, GET /odata/ComplaintOData returned a list with just Id and Name as expected, GET /odata/ComplaintOData(1) returned a single record containing Details in addition to the other two fields, also as expected.

I never had to change your code for the controller or the WebApiConfig.cs except the string parameter in builder.EntitySet<ComplaintTypeViewModel>("ComplaintOData"); which had to match the ccontroller (you have "ComplaintTypeOData" in your code).

Since it worked, I tried to figure out how I can reproduce your 406. I've changed ComplaintViewModel to extend ComplaintTypeViewModel directly instead of extending ComplaintOverviewViewModel (note that I've just duplicated the Name property):

public class ComplaintTypeViewModel
{
    public int Id { get; set; }
}

public class ComplaintOverviewViewModel : ComplaintTypeViewModel
{
    public string Name { get; set; }
}

public class ComplaintViewModel : ComplaintTypeViewModel
{
    public string Name { get; set; }
    public string Details { get; set; }
}

This worked just fine as well.

The only way I could reproduce your 406 is when I changed the ComplaintViewModel to not have ComplaintTypeViewModel in its inheritance hierarchy at all:

public class ComplaintTypeViewModel
{
    public int Id { get; set; }
}

public class ComplaintOverviewViewModel : ComplaintTypeViewModel
{
    public string Name { get; set; }
}

public class ComplaintViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Details { get; set; }
}

This finally gave me the 406 code from GET /odata/ComplaintOData(1) while GET /odata/ComplaintOData was still returning the list with Id and Name just fine.

So, it looks like as long as all of your ViewModel classes extend the same T from builder.EntitySet<T>("name") you can return any of them from Get() overloads in your controller. Please check how you define your ViewModels

  • Thank you. In the meantime I had placed my code in an `ApiController`. Thanks to this, I can move the code back to the `ODataController`. – Martijn Oct 10 '17 at 09:24
  • Unfortunately. I thought it worked, but it doesn't work when you query the model with odata. Could you try adding `?%24orderby=CreationDate%20desc&%24top=20&%24select=Id%2CSequenceNumber%2CCreationDate%2CCustomer%2CDescription&%24count=true` to the `Get()` method? PS: In my opening post ComplaintTypeViewModel should be ComplaintViewModel – Martijn Oct 10 '17 at 15:38
  • @Martijn, of course it works. You're escaping too much. You only need to escape the values of parameters, not names: `?$orderby=CreationDate%20desc&$top=20&$select=Id%2cSequenceNumber%2cCreationDate%2cCustomer%2cDescription&$count=true` – Michael Domashchenko Oct 10 '17 at 19:03
  • I don't get it to work. I've shortened the odata query to `?$orderby=CreationDate%20desc&$top=20&$count=true&$select=Id`. Things go wrong when I apply the `select`. Without the `select` everything works. The exception I get with `select` is `"The 'TypeAs' expression with an input of type '.ComplaintOverviewViewModel' and a check of type '.ComplaintBaseViewModel' is not supported. Only entity types and complex types are supported in LINQ to Entities queries."` – Martijn Oct 11 '17 at 07:46
  • Since I had to model your issue with my own code, I've used POCO arrays with `AsQueryable()`. That works with `$select`. If you're using EF behind your controller, it has its own implementation for `IQueryable` which might place other restrictions on your query. I cannot help you further without you providing more details on the internals of your implementation. There are answered questions in SO for that exception, for example:https://stackoverflow.com/questions/17242729/linq-throwing-typeas-expression-with-input-of-type-1-and-check-of-type-2-is-not – Michael Domashchenko Oct 11 '17 at 11:33
  • @ Michael Domashchenko I've updated my opening post with some code. Does this help? – Martijn Oct 11 '17 at 14:25
  • How are all your classes related? `class ComplaintOverviewViewModel : ComlaintBaseViewModel` and `class ComplaintViewModel : ComplaintBaseViewModel`? Which one do you use with the builder? `builder.EntitySet()`? You should use the base class: https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/complex-type-inheritance-in-odata-v4 – Michael Domashchenko Oct 12 '17 at 11:17
0

Create an umbrella class for both models and have your get methods return respectively what you want.

public class myMainModel
{
  public xxxOverviewViewModel x {get;set;}
  public xxxOverviewViewModel y {get;set;}
}

myMainModel Get()
{ 
  ....
  return myMainModel.x;
}

myMainModel Get( int key)
{ 
  ....
  return myMainModel.y;
}
apomene
  • 14,282
  • 9
  • 46
  • 72
  • @apemene I've tried this, but then I get an Exception saying that my model does not have a key defined. This Exception is thrown at `return builder.GetEdmModel` in the `WebApiConfig.cs`. `builder.EntitySet("ComplaintOData");` – Martijn Oct 04 '17 at 10:25
0

Try to just remove [EnableQuery] from action

public ComplaintViewModel Get([FromODataUri] int key)

EnableQueryAttribute enable querying using the OData query syntax. It is acceptable for collections IQueryable<>. When you want to return single entity, don't use that attribute.

UPDATES

Also try to rename parameter int key to id

Roman Marusyk
  • 23,328
  • 24
  • 73
  • 116
  • I have removed the `[EnableQuery]` attribute, but I still get the `406 Not Acceptable` message. – Martijn Oct 04 '17 at 10:24
  • @Martijn How do you call it? See this https://stackoverflow.com/a/27191598/4275342 – Roman Marusyk Oct 04 '17 at 10:39
  • I've modifed the code so that it matches exactly as described, but I still get a 406 :/ – Martijn Oct 04 '17 at 10:57
  • I call it from the browser like this: `http://localhost:63538/odata/ComplaintOData(1)` I've also tried Postman, with a get request, same result. – Martijn Oct 04 '17 at 11:29
  • @Martijn See my updates: Also try to rename parameter int key to id – Roman Marusyk Oct 04 '17 at 11:51
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/155917/discussion-between-martijn-and-megatron). – Martijn Oct 04 '17 at 13:16
  • I have tried your suggestion of replacing key with id, but with no success. One thing I noticed though is when I set a breakpoint in my controller, the breakpoint get hit, it is returning the data, and then I get a 406. This is using the `key` parameter name. – Martijn Oct 04 '17 at 13:20