web-dev-qa-db-fra.com

WebAPI et ODataController renvoient 406 non acceptable

Avant d'ajouter OData à mon projet, mes itinéraires étaient configurés comme ceci:

       config.Routes.MapHttpRoute(
            name: "ApiById",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional },
            constraints: new { id = @"^[0-9]+$" },
            handler: sessionHandler
        );

        config.Routes.MapHttpRoute(
            name: "ApiByAction",
            routeTemplate: "api/{controller}/{action}",
            defaults: new { action = "Get" },
            constraints: null,
            handler: sessionHandler
        );

        config.Routes.MapHttpRoute(
            name: "ApiByIdAction",
            routeTemplate: "api/{controller}/{id}/{action}",
            defaults: new { id = RouteParameter.Optional },
            constraints: new { id = @"^[0-9]+$" },
            handler: sessionHandler

Tous les contrôleurs fournissent Get, Put (le nom de l'action est Create), Patch (le nom de l'action est Update) et Delete. Par exemple, le client utilise ces différentes URL standard pour les demandes CustomerType:

string getUrl =  "api/CustomerType/{0}";
string findUrl = "api/CustomerType/Find?param={0}";
string createUrl = "api/CustomerType/Create";
string updateUrl = "api/CustomerType/Update";
string deleteUrl = "api/CustomerType/{0}/Delete";

Ensuite, j'ai ajouté un contrôleur OData avec les mêmes noms d'action que mes autres contrôleurs Api. J'ai également ajouté un nouvel itinéraire:

        ODataConfig odataConfig = new ODataConfig();

        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: null,
            model: odataConfig.GetEdmModel()
        );

Jusqu'à présent, je n'ai rien changé du côté client. Lorsque j'envoie une demande, j'obtiens une erreur 406 non disponible.

Les itinéraires se mélangent-ils? Comment puis-je résoudre ça?

32
Ivan-Mark Debono

L'ordre dans lequel les itinéraires sont configurés a un impact. Dans mon cas, j'ai également des contrôleurs MVC standard et des pages d'aide. Donc, dans Global.asax:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    GlobalConfiguration.Configure(config =>
    {
        ODataConfig.Register(config); //this has to be before WebApi
        WebApiConfig.Register(config); 

    });
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
}

Les parties filter et routeTable n'étaient pas là quand j'ai commencé mon projet et sont nécessaires.

ODataConfig.cs:

public static void Register(HttpConfiguration config)
{
    config.MapHttpAttributeRoutes(); //This has to be called before the following OData mapping, so also before WebApi mapping

    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();

    builder.EntitySet<Site>("Sites");
    //Moar!

    config.MapODataServiceRoute("ODataRoute", "api", builder.GetEdmModel());
}

WebApiConfig.cs:

public static void Register(HttpConfiguration config)
{
    config.Routes.MapHttpRoute( //MapHTTPRoute for controllers inheriting ApiController
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
    );
}

Et en bonus, voici mon RouteConfig.cs:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute( //MapRoute for controllers inheriting from standard Controller
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

Cela doit être dans ce ORDRE EXACT. J'ai essayé de déplacer les appels et je me suis retrouvé avec MVC, Api ou Odata cassé avec des erreurs 404 ou 406.

Je peux donc appeler:

localhost: xxx/-> conduit aux pages d'aide (home controller, page d'index)

localhost: xxx/api/-> conduit aux métadonnées OData $

localhost: xxx/api/Sites -> mène à la méthode Get de mes SitesController héritant d'ODataController

localhost: xxx/api/Test -> conduit à la méthode Get de mon TestController héritant d'ApiController.

17
Jerther

Si vous utilisez OData V4, remplacez using System.Web.Http.OData;

Avec using System.Web.OData; (Veuillez vérifier les commentaires pour la dernière bibliothèque)

dans ODataController fonctionne pour moi.

73
JeeShen Lee

Définissez routePrefix sur "api".

ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<CustomerType>("CustomerType");

config.MapODataServiceRoute(routeName: "ODataRoute", routePrefix: "api", model: builder.GetEdmModel());

Quelle version OData utilisez-vous? Vérifiez les espaces de noms corrects, pour OData V4 utilisez System.Web.OData, pour V3 System.Web.Http.OData. Les espaces de noms utilisés dans les contrôleurs doivent être cohérents avec ceux utilisés dans WebApiConfig.

11
martinoss

Mon problème était lié au retour du modèle d'entité au lieu du modèle que j'ai exposé (builder.EntitySet<ProductModel>("Products");). La solution consistait à mapper l'entité au modèle de ressource.

7
Steve Greene

Une autre chose à prendre en considération est que l'URL est sensible à la casse:

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

6
Ciprian Teiosanu

Aucune des excellentes solutions de cette page n'a fonctionné pour moi. En déboguant, j'ai pu voir que la route était récupérée et que les requêtes OData s'exécutaient correctement. Cependant, ils se déformaient après la sortie du contrôleur, ce qui suggérait que c'était le formatage qui générait ce qui semblait être l'erreur fourre-tout OData: 406 Not Acceptable.

J'ai corrigé cela en ajoutant un formateur personnalisé basé sur la bibliothèque Json.NET:

public class JsonDotNetFormatter : MediaTypeFormatter
{
    public JsonDotNetFormatter()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
    }

    public override bool CanReadType(Type type)
    {
        return true;
    }

    public override bool CanWriteType(Type type)
    {
        return true;
    }

    public override async Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
    {
        using (var reader = new StreamReader(readStream))
        {
            return JsonConvert.DeserializeObject(await reader.ReadToEndAsync(), type);
        }
    }

    public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
    {
        if (value == null) return;
        using (var writer = new StreamWriter(writeStream))
        {
            await writer.WriteAsync(JsonConvert.SerializeObject(value, new JsonSerializerSettings {ReferenceLoopHandling = ReferenceLoopHandling.Ignore}));
        }
    }

Puis dans WebApiConfig.cs, J'ai ajouté la ligne config.Formatters.Insert(0, new JsonDotNetFormatter()). Notez que je m'en tiens à l'ordre décrit dans la réponse de Jerther.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        ConfigureODataRoutes(config);
        ConfigureWebApiRoutes(config);
    }

    private static void ConfigureWebApiRoutes(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional });
    }

    private static void ConfigureODataRoutes(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();
        config.Formatters.Insert(0, new JsonDotNetFormatter());
        var builder = new ODataConventionModelBuilder();
        builder.EntitySet<...>("<myendpoint>");
        ...
        config.MapODataServiceRoute("ODataRoute", "odata", builder.GetEdmModel());
    }
}
4
Rob Lyndon

Le problème/solution dans mon cas était encore plus stupide. J'avais laissé du code de test dans mon action qui renvoyait un type de modèle complètement différent, juste un Dictionary, et pas mon type de modèle EDM approprié.

Bien que je proteste contre l'utilisation de HTTP 406 Not Acceptable communiquer l'erreur de mes voies, est tout aussi stupide.

3
Luke Puplett

Le problème que j'avais était que j'avais nommé mon entité "Produits" et que j'avais un ProductController. Il s'avère que le nom de l'ensemble d'entités doit correspondre au nom de votre contrôleur.

Alors

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

avec un contrôleur nommé ProductController donnera des erreurs.

/ api/Le produit donnera un 406

/ api/Les produits donneront un 404

Donc, en utilisant certaines des nouvelles fonctionnalités C # 6, nous pouvons le faire à la place:

builder.EntitySet<Product>(nameof(ProductsController).Replace("Controller", string.Empty));
3
Tikall

Trouvé dans l'erreur GitHub: " Impossible d'utiliser odata $ select, $ expand et autres par défaut # 511" , leur solution est de mettre ce qui suit AVANT d'enregistrer l'itinéraire:

// enable query options for all properties
config.Filter().Expand().Select().OrderBy().MaxTop(null).Count();

A fonctionné à merveille pour moi.

Source: https://github.com/OData/RESTier/issues/511

1
Josh Davidson

Mon erreur et mon correctif étaient différents des réponses ci-dessus.

Le problème spécifique que j'avais était d'accéder à un point de terminaison mediaReadLink dans mon ODataController dans WebApi 2.2.

OData a une propriété "flux par défaut" dans la spécification qui permet à une entité retournée d'avoir une pièce jointe. Donc, par exemple L'objet json pour filter etc décrit l'objet, puis il y a un lien multimédia intégré auquel on peut également accéder. Dans mon cas, il s'agit d'une version PDF de l'objet décrit.

Il y a quelques problèmes bouclés ici, le premier vient de la configuration:

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

Au début, j'essayais de renvoyer un FileStreamResult, mais je pense que ce n'est pas le runtime net45 par défaut. le pipeline ne peut donc pas le formater comme réponse, et un 406 inacceptable s'ensuit.

Le correctif ici était de renvoyer un HttpResponseMessage et de créer le contenu manuellement:

    [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;
    }

Mon dernier problème ici était d'obtenir une erreur 500 inattendue après avoir renvoyé un résultat valide. Après avoir ajouté un filtre d'exception général, j'ai trouvé que l'erreur était Queries can not be applied to a response content of type 'System.Net.Http.StreamContent'. The response content must be an ObjectContent.. Le correctif consistait à supprimer l'attribut [EnableQuery] Du haut de la déclaration du contrôleur et à ne l'appliquer qu'au niveau de l'action pour les points de terminaison qui renvoyaient des objets d'entité.

L'attribut [System.Web.Http.Route("myobjdownload")] est de savoir comment incorporer et utiliser des liens multimédias dans OData V4 à l'aide de l'API Web 2.2. Je vais vider la configuration complète de ceci ci-dessous pour être complet.

Tout d'abord, dans mon 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);
}

Le résultat final est que mes objets json intègrent ces propriétés odata:

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

Et ce qui suit, le lien multimédia décodé http://myhost/data/myobj/?id=myid/content Déclenche le point de terminaison sur votre MyObjController : ODataController.

1
user326608

Pour moi, le problème était que j'ai utilisé LINQ et sélectionné directement les objets chargés. Je devais utiliser select new pour que cela fonctionne:

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

Cela n'a pas fonctionné:

return Ok(from u in db.Users
          where u.UserId == key
          select u);
0
Florian K

Dans mon cas, je devais changer un setter de propriété non public en public.

public string PersonHairColorText { get; internal set; }

Doit être changé en:

public string PersonHairColorText { get; set; }
0
Jeremy Cook

Dans mon cas (odata V3), j'ai dû changer le nom d'OdataController pour qu'il soit identique à celui fourni dans ODataConventionModelBuilder et cela a résolu le problème.

mon contrôleur:

public class RolesController : ODataController
{
    private AngularCRMDBEntities db = new AngularCRMDBEntities();

    [Queryable]
    public IQueryable<tROLE> GetRoles()
    {
        return db.tROLEs;
    }
}

ODataConfig.cs:

public class ODataConfig
{
    public static void Register(HttpConfiguration config)
    {
        ODataConventionModelBuilder modelBuilder = new ODataConventionModelBuilder();
        modelBuilder.EntitySet<WMRole>("RolesNormal"); 
        modelBuilder.EntitySet<WMCommon.DAL.EF.tROLE>("Roles").EntityType.HasKey(o => o.IDRole).HasMany(t => t.tROLE_AUTHORIZATION);
        modelBuilder.EntitySet<WMCommon.DAL.EF.tLOOKUP>("Lookups").EntityType.HasKey(o => o.IDLookup).HasMany(t => t.tROLE_AUTHORIZATION);
        modelBuilder.EntitySet<WMCommon.DAL.EF.tROLE_AUTHORIZATION>("RoleAuthorizations").EntityType.HasKey(o => o.IDRoleAuthorization);

        config.Routes.MapODataRoute("odata", "odata", modelBuilder.GetEdmModel());
        config.EnableQuerySupport();
    }
}

WebApiConfig.cs:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();
        config.SuppressDefaultHostAuthentication();
        config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));            

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

        var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
        jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

        GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings
            .ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
        GlobalConfiguration.Configuration.Formatters
            .Remove(GlobalConfiguration.Configuration.Formatters.XmlFormatter);
    }
}

Global.asax:

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        GlobalConfiguration.Configure(config =>
        {
            ODataConfig.Register(config); 
            WebApiConfig.Register(config);
        });            
    }
}
0
user1892777