WebAPI and ODataController return 406 Not Acceptable

前端 未结 13 1146
余生分开走
余生分开走 2020-12-02 12:25

Before adding OData to my project, my routes where set up like this:

       config.Routes.MapHttpRoute(
            name: \"ApiById\",
            routeTempl         


        
相关标签:
13条回答
  • 2020-12-02 12:52

    My error and fix was different from the answers above.

    The specific issue I had was accessing a mediaReadLink endpoint in my ODataController in WebApi 2.2.

    OData has a 'default stream' property in the spec which allows a returned entity to have an attachment. So the e.g. json object for filter etc describes the object, and then there is a media link embedded which can also be accessed. In my case it is a PDF version of the object being described.

    There's a few curly issues here, the first comes from the config:

    <system.web>
      <customErrors mode="Off" />
      <compilation debug="true" targetFramework="4.7.1" />
      <httpRuntime targetFramework="4.5" />
      <!-- etc -->
    </system.web>
    

    At first I was trying to return a FileStreamResult, but i believe this isn't the default net45 runtime. so the pipeline can't format it as a response, and a 406 not acceptable ensues.

    The fix here was to return a HttpResponseMessage and build the content manually:

        [System.Web.Http.HttpGet]
        [System.Web.Http.Route("myobjdownload")]
        public HttpResponseMessage DownloadMyObj(string id)
        {
            try
            {
                var myObj = GetMyObj(id); // however you do this                
                if (null != myObj )
                {
                    HttpResponseMessage result = Request.CreateResponse(HttpStatusCode.OK);
    
                    byte[] bytes = GetMyObjBytes(id); // however you do this
                    result.Content = new StreamContent(bytes); 
    
                    result.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/pdf");
                    result.Content.Headers.LastModified = DateTimeOffset.Now;  
                    result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment)
                    {
                        FileName = string.Format("{0}.pdf", id),
                        Size = bytes.length,
                        CreationDate = DateTimeOffset.Now,
                        ModificationDate = DateTimeOffset.Now
                    };
    
                     return  result;
                }
            }
            catch (Exception e)
            {
                // log, throw 
            }
            return null;
        }
    

    My last issue here was getting an unexpected 500 error after returning a valid result. After adding a general exception filter I found the error was Queries can not be applied to a response content of type 'System.Net.Http.StreamContent'. The response content must be an ObjectContent.. The fix here was to remove the [EnableQuery] attribute from the top of the controller declaration, and only apply it at the action level for the endpoints that were returning entity objects.

    The [System.Web.Http.Route("myobjdownload")] attribute is how to embed and use media links in OData V4 using web api 2.2. I'll dump the full setup of this below for completeness.

    Firstly, in my Startup.cs:

    [assembly: OwinStartup(typeof(MyAPI.Startup))]
    namespace MyAPI
    {
        public class Startup
        {
            public void Configuration(IAppBuilder app)
            {
                // DI etc
                // ...
                GlobalConfiguration.Configure(ODataConfig.Register); // 1st
                GlobalConfiguration.Configure(WebApiConfig.Register); // 2nd      
                // ... filters, routes, bundles etc
                GlobalConfiguration.Configuration.EnsureInitialized();
            }
        }
    }
    

    ODataConfig.cs:

    // your ns above
    public static class ODataConfig
    {
        public static void Register(HttpConfiguration config)
        {
            ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
            var entity1 = builder.EntitySet<MyObj>("myobj");
            entity1.EntityType.HasKey(x => x.Id);
            // etc
    
            var model = builder.GetEdmModel();
    
            // tell odata that this entity object has a stream attached
            var entityType1 = model.FindDeclaredType(typeof(MyObj).FullName);
            model.SetHasDefaultStream(entityType1 as IEdmEntityType, hasStream: true);
            // etc
    
            config.Formatters.InsertRange(
                                        0, 
                                        ODataMediaTypeFormatters.Create(
                                                                        new MySerializerProvider(),
                                                                        new DefaultODataDeserializerProvider()
                                                                        )
                                        );
    
            config.Select().Expand().Filter().OrderBy().MaxTop(null).Count();
    
            // note: this calls config.MapHttpAttributeRoutes internally
            config.Routes.MapODataServiceRoute("ODataRoute", "data", model);
    
            // in my case, i want a json-only api - ymmv
            config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeWithQualityHeaderValue("text/html"));
            config.Formatters.Remove(config.Formatters.XmlFormatter);
    
        }
    }
    

    WebApiConfig.cs:

    // your ns above
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // https://stackoverflow.com/questions/41697934/catch-all-exception-in-asp-net-mvc-web-api
            //config.Filters.Add(new ExceptionFilter());
    
            // ymmv
            var cors = new EnableCorsAttribute("*", "*", "*");
            config.EnableCors(cors);
    
            // so web api controllers still work
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
    
            // this is the stream endpoint route for odata
            config.Routes.MapHttpRoute("myobjdownload", "data/myobj/{id}/content", new { controller = "MyObj", action = "DownloadMyObj" }, null);
            // etc MyObj2
        }
    }
    

    MySerializerProvider.cs:

    public class MySerializerProvider: DefaultODataSerializerProvider
    {
        private readonly Dictionary<string, ODataEdmTypeSerializer> _EntitySerializers;
    
        public SerializerProvider()
        {
            _EntitySerializers = new Dictionary<string, ODataEdmTypeSerializer>();
            _EntitySerializers[typeof(MyObj).FullName] = new MyObjEntitySerializer(this);
            //etc 
        }
    
        public override ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
        {
            if (edmType.IsEntity())
            {
                string stripped_type = StripEdmTypeString(edmType.ToString());
                if (_EntitySerializers.ContainsKey(stripped_type))
                {
                    return _EntitySerializers[stripped_type];
                }
            }            
            return base.GetEdmTypeSerializer(edmType);
        }
    
        private static string StripEdmTypeString(string t)
        {
            string result = t;
            try
            {
                result = t.Substring(t.IndexOf('[') + 1).Split(' ')[0];
            }
            catch (Exception e)
            {
                //
            }
            return result;
        }
    }
    

    MyObjEntitySerializer.cs:

    public class MyObjEntitySerializer : DefaultStreamAwareEntityTypeSerializer<MyObj>
    {
        public MyObjEntitySerializer(ODataSerializerProvider serializerProvider) : base(serializerProvider)
        {
        }
    
        public override Uri BuildLinkForStreamProperty(MyObj entity, EntityInstanceContext context)
        {
            var url = new UrlHelper(context.Request);
            string id = string.Format("?id={0}", entity.Id);
            var routeParams = new { id }; // add other params here
            return new Uri(url.Link("myobjdownload", routeParams), UriKind.Absolute);            
        }
    
        public override string ContentType
        {
            get { return "application/pdf"; }            
        }
    }
    

    DefaultStreamAwareEntityTypeSerializer.cs:

    public abstract class DefaultStreamAwareEntityTypeSerializer<T> : ODataEntityTypeSerializer where T : class
    {
        protected DefaultStreamAwareEntityTypeSerializer(ODataSerializerProvider serializerProvider)
            : base(serializerProvider)
        {
        }
    
        public override ODataEntry CreateEntry(SelectExpandNode selectExpandNode, EntityInstanceContext entityInstanceContext)
        {
            var entry = base.CreateEntry(selectExpandNode, entityInstanceContext);
    
            var instance = entityInstanceContext.EntityInstance as T;
    
            if (instance != null)
            {
                entry.MediaResource = new ODataStreamReferenceValue
                {
                    ContentType = ContentType,
                    ReadLink = BuildLinkForStreamProperty(instance, entityInstanceContext)
                };
            }
            return entry;
        }
    
        public virtual string ContentType
        {
            get { return "application/octet-stream"; }
        }
    
        public abstract Uri BuildLinkForStreamProperty(T entity, EntityInstanceContext entityInstanceContext);
    }
    

    The end result is my json objects get these odata properties embedded:

    odata.mediaContentType=application/pdf
    odata.mediaReadLink=http://myhost/data/myobj/%3fid%3dmyid/content
    

    And the following the decoded media link http://myhost/data/myobj/?id=myid/content fires the endpoint on your MyObjController : ODataController.

    0 讨论(0)
  • 2020-12-02 12:53

    The problem I had was that i had named my entityset "Products" and had a ProductController. Turns out the name of the entity set must match your controller name.

    So

    builder.EntitySet<Product>("Products");
    

    with a controller named ProductController will give errors.

    /api/Product will give a 406

    /api/Products will give a 404

    So using some of the new C# 6 features we can do this instead:

    builder.EntitySet<Product>(nameof(ProductsController).Replace("Controller", string.Empty));
    
    0 讨论(0)
  • 2020-12-02 12:54

    In my case I needed to change a non-public property setter to public.

    public string PersonHairColorText { get; internal set; }
    

    Needed to be changed to:

    public string PersonHairColorText { get; set; }
    
    0 讨论(0)
  • 2020-12-02 12:56

    For me the problem was, that I used LINQ and selected the loaded objects directly. I had to use select new for it to work:

    return Ok(from u in db.Users
              where u.UserId == key
              select new User
              {
                  UserId = u.UserId,
                  Name = u.Name
              });
    

    This did not work:

    return Ok(from u in db.Users
              where u.UserId == key
              select u);
    
    0 讨论(0)
  • 2020-12-02 12:59

    Set routePrefix to "api".

    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<CustomerType>("CustomerType");
    
    config.MapODataServiceRoute(routeName: "ODataRoute", routePrefix: "api", model: builder.GetEdmModel());
    

    Which OData version are you using? Check for correct namespaces, for OData V4 use System.Web.OData, for V3 System.Web.Http.OData. Namespaces used in controllers have to be consistent with the ones used in WebApiConfig.

    0 讨论(0)
  • 2020-12-02 12:59

    Another thing to be taken into consideration is that the URL is case sensitive so:

    localhost:xxx/api/Sites -> OK
    localhost:xxx/api/sites -> HTTP 406

    0 讨论(0)
提交回复
热议问题