WebAPI et ODataController retour 406 inacceptable
avant D'ajouter OData à mon projet, mes itinéraires ont été mis en place 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 d'action est Create), Patch (le nom d'action est Update) et Delete. À titre d'exemple, le client utilise ces différentes URLs standard pour les requêtes 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";
puis j'ai ajouté un contrôleur OData avec les mêmes noms d'action que mes autres contrôleurs Api. J'ai aussi ajouté une nouvelle route:
ODataConfig odataConfig = new ODataConfig();
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
model: odataConfig.GetEdmModel()
);
jusqu'à présent, je n'ai rien changé du côté du client. Quand j'envoie une demande, j'obtiens une erreur 406 Non disponible.
est-ce que les routes se confondent? Comment je peux résoudre ça?
13 réponses
l'ordre dans lequel les routes sont configurées a un impact. Dans mon cas, j'ai aussi des contrôleurs MVC standard et des pages d'aide. Ainsi, 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);
}
le filtre et les pièces routables n'étaient pas là quand j'ai commencé mon projet et sont nécessaire .
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 prime, 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 cet ordre EXACT . J'ai essayé de déplacer les appels et j'ai fini avec MVC, Api ou Odata cassés avec des erreurs 404 ou 406.
pour que je puisse appeler:
localhost: xxx / - > conduit à des pages d'aide (Home controller, page index)
localhost:xxx/api/ -> mène à la OData $métadonnées
localhost: xxx / api / Sites - > conduit à la méthode Get de mon SitesController hériter de ODataController
localhost: xxx/api / Test -> conduit à la méthode Get de mon TestController héritant D'ApiController.
si vous utilisez OData V4, remplacer using System.Web.Http.OData;
avec using System.Web.OData;
dans le ODataController fonctionne pour moi.
définit routePrefix à"api".
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<CustomerType>("CustomerType");
config.MapODataServiceRoute(routeName: "ODataRoute", routePrefix: "api", model: builder.GetEdmModel());
quelle version D'OData utilisez-vous? Vérifiez les espaces de noms corrects, pour OData v4 utilisez System.Web.OData
, pour V3 System.Web.Http.OData
. Les Namespaces utilisés dans les controllers doivent être compatibles avec ceux utilisés dans WebApiConfig.
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
mon problème concernait le retour du modèle d'entité au lieu du modèle que j'exposais ( builder.EntitySet<ProductModel>("Products");
). La Solution était de faire correspondre le modèle d'entité aux ressources.
aucune des excellentes solutions sur cette page n'a fonctionné pour moi. En déboguant, j'ai pu voir que la route était reprise et que les requêtes OData fonctionnaient correctement. Toutefois, ils étaient en train de se mutiler après la sortie du contrôleur, ce qui laissait entendre que c'était le formatage qui produisait ce qui semble être L'erreur fourre-tout D'OData: 406 inacceptable.
j'ai corrigé cela en ajoutant un formatteur personnalisé basé sur le Json.NET bibliothèque:
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());
}
}
Le problème que j'avais était que j'avais nommé mon entityset "Produits", et un ProductController. S'avère que le nom de l'entité doit correspondre à votre nom de contrôleur.
Donc
builder.EntitySet<Product>("Products");
avec un contrôleur nommé ProductController donnera des erreurs.
/api/Produit va donner une 406
/api/Produits donnera une erreur 404
donc en utilisant certaines des nouvelles fonctionnalités C# 6 Nous pouvons faire ceci à la place:
builder.EntitySet<Product>(nameof(ProductsController).Replace("Controller", string.Empty));
Le problème/solution dans mon cas était encore plus stupide. J'avais laissé un code de test dans mon action qui renvoyait un type de modèle complètement différent, juste un Dictionary
, et pas mon propre type de modèle EDM.
bien que je proteste que L'utilisation de HTTP 406 Not Acceptable
pour communiquer l'erreur de mes voies, est tout aussi stupide.
trouvé dans l'erreur GitHub: " incapable d'utiliser odata $ select, $expand, et autres par défaut #511" , leur solution est de mettre la ligne suivante avant d'enregistrer la route:
// enable query options for all properties
config.Filter().Expand().Select().OrderBy().MaxTop(null).Count();
a Fonctionné comme un charme pour moi.
dans mon cas, j'ai eu besoin de changer un bien non public setter au public.
public string PersonHairColorText { get; internal set; }
Il fallait remplacer par
."public string PersonHairColorText { get; set; }
mon erreur et ma correction étaient différentes des réponses ci-dessus.
le problème précis que j'avais était l'accès à un mediaReadLink
endpoint dans mon ODataController dans WebApi 2.2.
OData possède une propriété 'default stream' dans la spécification qui permet à une entité retournée d'avoir une pièce jointe. Ainsi, l'objet par exemple json pour filter
etc décrit l'objet, et puis il y a un lien multimédia intégré qui peut également être accédé. Dans mon cas, c'est un PDF la version de l'objet décrit.
il y a quelques problèmes bouclés ici, le premier vient de la config:
<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 retourner un FileStreamResult
, mais je crois que ce n'est pas l'exécution par défaut de net45. donc le pipeline ne peut pas le formater comme une réponse, et un 406 non acceptable s'ensuit.
la solution ici était de retourner un HttpResponseMessage
et construire 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 inattendue de 500 après avoir retourné un résultat valide. Après avoir ajouté un filtre d'exception générale, 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 ici était de supprimer l'attribut [EnableQuery]
du haut de la déclaration du contrôleur, et de l'appliquer seulement au niveau d'action pour les endpoints qui retournaient des objets entity.
l'attribut [System.Web.Http.Route("myobjdownload")]
indique comment intégrer et utiliser des liens média dans OData V4 en utilisant l'api web 2.2. Je vais jeter la configuration complète de ceci ci-dessous pour l'exhaustivité.
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)
{
// /q/catch-all-exception-in-asp-net-mvc-web-api-duplicate-41856/"*", "*", "*");
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 mes objets JSON obtenir ces propriétés odata intégré:
odata.mediaContentType=application/pdf
odata.mediaReadLink=http://myhost/data/myobj/%3fid%3dmyid/content
et le lien média décodé suivant http://myhost/data/myobj/?id=myid/content
renvoie le point final sur votre MyObjController : ODataController
.
dans mon cas (odata V3) j'ai dû changer le nom D'OdataController pour être le même que prévu dans ODataConventionModelBuilder et qui 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);
});
}
}
pour moi le problème était que J'utilisais LINQ et sélectionnais les objets chargés directement.
J'ai dû 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
});
Ce n' pas "151970920 de travail":
return Ok(from u in db.Users
where u.UserId == key
select u);