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?
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.
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.
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.
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.
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
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());
}
}
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.
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));
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.
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
.
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);
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; }
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);
});
}
}