0

I've looked at all the SO posts on this and had no success. I'm able to call into the OData controller and see the data retrieved, mapped, and packaged-up. The controller method exits and I get a 406 - Not Acceptable. I'm only using the System.Web.OData namespace (OData Version v4.0.30319) and, using Postman, I manually set the Content and Accept headers to 'application/json' with no luck

Maybe I've missed something over the last 2 hours reading everyone's posts on similar problems. Any pointers will be appreciated.

UPDATE: The problem appears to be in the Mapper code (Automapper) as pointed out by Igor below. It looks like there's a commitment to return the database(EF) entity, not a mapped class. With this knowledge I found this SO post, but it doesn't offer a solution: ApiController vs ODataController when exposing DTOs. Are we stuck having to return database entities or can the results be mapped? If so, that's a deal breaker for me.

Controller:

[EnableQuery]
[HttpGet]
public async Task<IHttpActionResult> Get()
    {
        var list = await db.ConfigSets.ToListAsync();
        Mapper.CreateMap<ConfigSet, ConfigSetDTO>();
        var configSetDTOs = Mapper.Map<List<ConfigSet>, List<ConfigSetDTO>>(list);
        return Ok(configSetDTOs); //IT LOOKS GOOD HERE!
    }

WebApiConfig:

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

            // OData - must be before config.Routes when using a prefix. In this case "api"
            config.MapODataServiceRoute("odata", "api", GetEdmModel(), new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));
            config.EnsureInitialized();

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

        private static IEdmModel GetEdmModel()
        {
            var builder = new ODataConventionModelBuilder();
            builder.Namespace = "Services";
            builder.ContainerName = "DefaultContainer";
            builder.EntitySet<ConfigSet>("Configuration");
            var edmModel = builder.GetEdmModel();
            return edmModel;
        }
    }
Community
  • 1
  • 1
Big Daddy
  • 5,160
  • 5
  • 46
  • 76
  • I am not sure what `Mapper.Map` returns but whatever it is it should implement `IQueryable` or the generic there of. Have you tried testing it with a hard coded `List` instance and in the `return` call do return `Ok(myList.AsQueryable());` ? That should help you narrow down where the issue is, overall configuration or with the data or format of the data you are trying to return. – Igor Dec 23 '15 at 18:57
  • @Igor..Mapper is Automapper. I tried this code without it and what you suggested and get the same result. – Big Daddy Dec 23 '15 at 19:03
  • I think I see it. `builder.EntitySet("Configuration");` denotes that the expected type that should be returned from the Get() method is of type `ConfigSet` but you are returning type `ConfigSetDTO`. To see if that is indeed the problem try returning a hard coded list of `ConfigSet` instances but call `AsQueryable()` in the `Ok` method. Ie; `return Ok(myConfigSetList.AsQueryable());` – Igor Dec 23 '15 at 19:11
  • @Igor...you're onto something. The mapping must be the problem because this works: var list = await db.ConfigSets.ToListAsync();return Ok(list); – Big Daddy Dec 23 '15 at 19:23
  • I have never used a Get() function from oData with anything other than EF data that I pull from a SQL view so I am not sure what is or is not possible. The whole purpose, as I understand it, is that the MS Odata Web API controller handles all of the query logic based on the incoming URL which is why you would want to use oData with a Get-er function as it provides a standard way to query the data, no extra code necessary for filtering back end, and filtering occurs on the database and not in memory. If you push something like Automapper between this I doubt it would work the same. – Igor Dec 23 '15 at 19:26
  • @Igor...good points. But now the database entities are bleeding into other layers - not good. I need my DTOs. This would be reason enough for me not to use OData controllers. – Big Daddy Dec 23 '15 at 19:33
  • That is correct and is one of the main complaints and drawbacks of using OData controllers. Personally I use it strictly with a very select number of sql Views that I want to query in various ways so I don't have to maintain back end code that deals with filtering, sorting, paging, etc which I do not consider business logic to begin with in my application(s). I do not do anything else with oData precisely because of what you mention. – Igor Dec 23 '15 at 20:03
  • @BigDaddy I am facing the same issue, did you find any solution for this?Please let me know. – Uddhao Pachrne Oct 12 '16 at 16:49
  • @UddhaoPachrne...Sorry, I didn't – Big Daddy Oct 13 '16 at 10:54

2 Answers2

1

I have never used a Get() function from oData with anything other than EF data that I pull from a SQL view so I am not sure what is or is not possible. The whole purpose, as I understand it, is that the MS Odata Web API controller handles all of the query logic based on the incoming URL which is why you would want to use oData with a Get method as it provides a standard way to query the data, no extra code necessary for filtering back end, and filtering occurs on the database and not in memory. If you push something like Automapper between this I doubt it would work the same.

Igor
  • 60,821
  • 10
  • 100
  • 175
1

The older versions of OData allow you to just expose any old IQueryable, whether it contains database objects or DTOs.

However, I can definitely say that you are using AutoMapper wrong here - you are doing the mapping within the client, rather than on the database, which would still allow you to return an IQueryable. Instead of using Mapper.Map, use the .ProjectTo<> extension method on your query:

using AutoMapper.QueryableExtensions;  // This using is required

[EnableQuery]
[HttpGet]
public IHttpActionResult Get()
{
    var query = db.ConfigSets.ProjectTo<ConfigSetDTO>();
    return Ok(query);
}

You should also not be defining your maps in action methods - they should be defined once at your application startup.

Richard
  • 29,854
  • 11
  • 77
  • 120
  • I'm still getting a 406 error with these changes. The mapping is now happening via an Automapper Profile called from Global.asax. Any ideas? – Big Daddy Dec 24 '15 at 13:23
  • I think your proposal is promising and makes sense, but I'm getting a 406. – Big Daddy Dec 28 '15 at 15:10