I've been trying to use Namespace routing to build some APIs dynamically without the need to worry about hardcoding the routes. However, I did find an example from MSDN to use namespaces and folder structure as your API structure. Here's the sample that I have to use Namespace routing:
public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
{
private readonly string _baseNamespace;
public NamespaceRoutingConvention(string baseNamespace)
{
_baseNamespace = baseNamespace;
}
public void Apply(ControllerModel controller)
{
var hasRouteAttributes = controller.Selectors.Any(selector => selector.AttributeRouteModel != null);
if (hasRouteAttributes)
{
return;
}
var namespc = controller.ControllerType.Namespace;
if (namespc == null) return;
var templateParts = new StringBuilder();
templateParts.Append(namespc, _baseNamespace.Length + 1, namespc.Length - _baseNamespace.Length - 1);
templateParts.Replace('.', '/');
templateParts.Append("/[controller]/[action]/{environment}/{version}");
var template = templateParts.ToString();
foreach (var selector in controller.Selectors)
{
selector.AttributeRouteModel = new AttributeRouteModel()
{
Template = template
};
}
}
}
And here's the controller:
namespace Backend.Controllers.Api.Project.Core
{
public class UserController : ApiBaseController
{
public UserController()
{
}
[HttpPost]
public IActionResult Login(LoginInput loginInput) // <-- loginInput properties return null
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
}
}
in Startup.cs
namespace Backend
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Let's use namespaces as the routing default way for our APIs
services.AddControllers(options =>
{
options.Conventions.Add(new NamespaceRoutingConvention(typeof(Startup).Namespace + ".Controllers"));
});
}
}
}
Everything works ok except that when I trigger a POST api call to Login action the LoginInput doesn't get populated the values I'm sending through Postman i.e. {"username": "value", "password": "sample"} and it always returns null value. I'm not sure what am I doing wrong with the NamespaceRoutingConvention. Bear in mind if I remove it and hard-code the route in the controller like:
[ApiController]
[Route("api/project/core/[controller]/[action]/proda/v1")]
It works as expected. Any ideas?
Try to use this instead:
[HttpPost]
public IActionResult Login([FromBody]LoginInput loginInput)
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
I think that by setting AttributeRouteModel, you're preventing the middleware invoked by having ApiControllerAttribute in the Controller to do its job, and so the defaults of treating object parameters as body is not applied.
This is a guess though, I haven't been able to find the corresponding code in the source code.
i am working with ASP.Net web API's.I have created a folder named api in controllers folder and then created an Api controller in that api folder.
In the admin controller i have simply placed the following code to check wheather the api is working or not.
public class AdminController : ApiController
{
DBEntities _context;
public AdminController()
{
_context = new DBEntities();
}
[HttpGet]
public IEnumerable<string> GetUsers()
{
return new string[] { "Muhammad","Ali"};
}
}
Then from browser i am calling http://localhost:57368/api/admin but it gives me "The resource cannot be found" with http 404 error code.It should atleast return JSON result but gives this error instead.
Any help is highly appreciated.
And when i go to network tab in chrome it shows the following details of the request:
protected void Application_Start()
{
GlobalConfiguration.Configure(config =>
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
});
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
Your global.asax.cs should look like this
[RoutePrefix("api/admin")]
public class AdminController : ApiController
{
DBEntities _context;
public AdminController()
{
_context = new DBEntities();
}
[Route("GetUsers")]
[HttpGet]
public IEnumerable<string> GetUsers()
{
return new string[] { "Muhammad","Ali"};
}
}
URL: http://localhost:57368/api/admin/GetUsers
you should use 'RoutePrefix' and 'Route' attributes. API should know how to navigate the incoming request to an action. this attributes help you. don't forget add this namespace ' using System.Web.Http'.
using System.Web.Http
[RoutePrefix("api/admin")]
public class AdminController : ApiController
{
DBEntities _context;
public AdminController()
{
_context = new DBEntities();
}
[HttpGet]
[Route("GetUsers")]
public IEnumerable<string> GetUsers()
{
return new string[] { "Muhammad","Ali"};
}
}
In the Global.asax i registered Api using GlobalConfiguration.Configure(WebApiConfig.Register)
but issue was that i placed it at the bottom due to which MVC took precedence and when i called api/admin it gave error saying no resourse was found beacuse MVC was in action.
I added this GlobalConfiguration.Configure(WebApiConfig.Register) at the top and in ApplicationStart() in Gloabl.asax and it worked fine.
I have an ASP.Net WebApi2 project hosting odata both ApiController and ODataController.
And I want to add a custom action in an ODataController.
I saw this seems to be achievable by either adding [HttpPost] attribute on the desired action, or by configuring the ODataConventionModelBuilder with a specific FunctionConfiguration when using the MapODataServiceRoute.
To distinguish between odata routes and webapi routes we use the following scheme :
odata : http://localhost:9292/myProject/odata
webapi : http://localhost:9292/myProject/api
I tried both these solution without success which all led to get an HTTP 404 result.
My custom action is defined as following:
public class SomeModelsController : ODataController
{
//...
[EnableQuery]
public IHttpActionResult Get()
{
//...
return Ok(data);
}
public IHttpActionResult MyCustomAction(int parameterA, int parameterB)
{
//...
return Json(data);
}
//...
}
So as you guessed it, the Get call on the controller perfectly work with odata. However the MyCustomAction is a bit more difficult to setup properly.
Here is what I have tried :
Setting an [HttpPost] attribute on MyCustomAction
[HttpPost]
public IHttpActionResult MyCustomAction(int parameterA, int parameterB)
{
//...
return Json(data);
}
I also tried decorating MyCustomAction with the [EnableQuery] attribute.
Also, I tried adding the [AcceptVerbs("GET", "POST")] attribute on the method without changes.
Configuring the ODataConventionModelBuilder
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder
{
Namespace = "MyApp",
ContainerName = "DefaultContainer"
};
// List of entities exposed and their controller name
// ...
FunctionConfiguration function = builder.Function("MyCustomAction ").ReturnsFromEntitySet<MyModel>("SomeModels");
function.Parameter<int>("parameterA");
function.Parameter<int>("parameterB");
function.Returns<MyModel>();
return builder.GetEdmModel();
}
Also tried decoration of MyCustomAction with [EnableQuery], HttpPost and [AcceptVerbs("GET", "POST")] attributes.
I still get HTTP 404 result.
My query url is as follow:
http://localhost:9292/myProject/odata/SomeModels/MyCustomAction?parameterA=123¶meterB=123
I also tried to POST parameters on
http://localhost:9292/myProject/odata/SomeModels/MyCustomAction with the same result. Actually with or without parameters I get HTTP 404 status.
I've created a working example from scratch with Visual Studio 2017.
If you want more info you can read this tutorial:
https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/odata-actions-and-functions
Create a new ASP.Net Web Application (no .Net Core)
Choose WebApi Template
Install from NuGet the package Microsoft.AspNet.OData (I have used v. 6.0.0)
Create a simple model class into Models folder
TestModel.cs
namespace DemoOdataFunction.Models
{
public class TestModel
{
public int Id { get; set; }
public int MyProperty { get; set; }
public string MyString { get; set; }
}
}
Configure WebApiConfig
WebApiConfig.cs
using DemoOdataFunction.Models;
using System.Web.Http;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
namespace DemoOdataFunction
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.Namespace = "MyNamespace";
builder.EntitySet<TestModel>("TestModels");
ActionConfiguration myAction = builder.EntityType<TestModel>().Action("MyAction");
myAction.Parameter<string>("stringPar");
FunctionConfiguration myFunction = builder.EntityType<TestModel>().Collection.Function("MyFunction");
myFunction.Parameter<int>("parA");
myFunction.Parameter<int>("parB");
myFunction.ReturnsFromEntitySet<TestModel>("TestModels");
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: "odata",
model: builder.GetEdmModel()
);
}
}
}
Create the controller TestModelsController into Controllers folder
TestModelsController.cs
using DemoOdataFunction.Models;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Web.OData;
using System.Web.OData.Query;
namespace DemoOdataFunction.Controllers
{
public class TestModelsController : ODataController
{
IQueryable<TestModel> testModelList = new List<TestModel>()
{
new TestModel{
MyProperty = 1,
MyString = "Hello"
}
}.AsQueryable();
[EnableQuery]
public IQueryable<TestModel> Get()
{
return testModelList;
}
[EnableQuery]
public SingleResult<TestModel> Get([FromODataUri] int key)
{
IQueryable<TestModel> result = testModelList.Where(t => t.MyProperty == 1);
return SingleResult.Create(result);
}
[HttpPost]
public IHttpActionResult MyAction([FromODataUri] int key, ODataActionParameters parameters)
{
string stringPar = parameters["stringPar"] as string;
return Ok();
}
[HttpGet]
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All, MaxExpansionDepth = 2)]
public IHttpActionResult MyFunction(int parA, int parB)
{
return Ok(testModelList);
}
}
}
Edit Web.config changing the handlers section in system.webServer
web.config
<system.webServer>
<handlers>
<clear/>
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="/*"
verb="*" type="System.Web.Handlers.TransferRequestHandler"
preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
[...]
</system.webServer>
That's all.
This is the request for MyAction:
POST
http://localhost:xxxx/odata/TestModels(1)/MyNamespace.MyAction
{
"stringPar":"hello"
}
This is the request for MyFunction:
GET
http://localhost:xxxx/odata/TestModels/MyNamespace.MyFunction(parA=1,parB=2)
I am using HTTP POST with route on the controller functions like below:
[HttpPost]
[Route("{application}/{envName}/date/{offset}")]
[ResponseType(typeof(DateInfo))]
public async Task<IHttpActionResult> SetDateOffsetForEnvironmentName(string application, string envName, string offset)
{
}
can you try setting the route on the function and then call the post method on it like this:
POST /status/environments/ATOOnline/PTH/date/0
Also try and capture a request through Fiddler and see what is being passed.
I'm encountering an issue with CORS while using IAsyncResourceFilter implementation.
I want to be able to call my actions from other domains as well...
I've defined the CORS policy under my Startup file as the following:
services.AddCors(options =>
{
options.AddPolicy("AllowAllOrigins",
builder =>
{
builder.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
And under the Configure method:
app.UseCors("AllowAllOrigins");
It works fine without using a TypeFilterAttribute which use IAsyncResourceFilter.
For example calling my API action without any TypeFilterAttribute attribute works:
public bool Get()
{
return true;
}
But when adding my TypeFilterAttribute as follows it doesn't work and returns the error about the CORS:
[MyTypeFilterAttribute("test")]
public bool Get()
{
return true;
}
Anything I'm missing? What should I add when using IAsyncResourceFilter?
The following is the MyTypeFilterAttribute code: (With no real logic...)
public class MyTypeFilterAttribute : TypeFilterAttribute
{
public MyTypeFilterAttribute(params string[] name) : base(typeof(MyTypeFilterAttributeImpl))
{
Arguments = new[] { new MyTypeRequirement(name) };
}
private class MyTypeFilterAttributeImpl: Attribute, IAsyncResourceFilter
{
private readonly MyTypeRequirement_myTypeRequirement;
public MyTypeFilterAttributeImpl(MyTypeRequirement myTypeRequirement)
{
_myTypeRequirement= myTypeRequirement;
}
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
context.Result = new OkResult();
await next();
}
}
}
public class MyTypeRequirement : IAuthorizationRequirement
{
public string Name { get; }
public MyTypeRequirement(string name)
{
Name = name;
}
}
Cors middleware sets headers on the response result object.
I believe you are resetting these with context.Result = new OkResult();
See poke's reply below. If you set any result in an action filter, this result gets sent back immediately, thus overwriting any other one!
Before adding OData to my project, my routes where set up like this:
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
All controllers provide Get, Put (action name is Create), Patch (action name is Update) and Delete. As an example, the client uses these various standard url's for the CustomerType requests:
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";
Then I added an OData controller with the same action names as my other Api controllers. I also added a new route:
ODataConfig odataConfig = new ODataConfig();
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
model: odataConfig.GetEdmModel()
);
So far I changed nothing on the client side. When I send a request, I get a 406 Not Available error.
Are the routes getting mixed up? How can I solve this?
If you are using OData V4, replace using System.Web.Http.OData;
With using Microsoft.AspNet.OData; (Please check the comments for the latest library)
in the ODataController works for me.
The order in which the routes are configured has an impact. In my case, I also have some standard MVC controllers and help pages. So in 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);
}
The filter and routeTable parts weren't there when I started my project and are needed.
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 }
);
}
And as a bonus, here's my 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 }
);
}
This has to be in that EXACT ORDER. I tried moving the calls around and ended up with either MVC, Api or Odata broken with 404 or 406 errors.
So I can call:
localhost:xxx/ -> leads to help pages (home controller, index page)
localhost:xxx/api/ -> leads to the OData $metadata
localhost:xxx/api/Sites -> leads to the Get method of my SitesController inheriting from ODataController
localhost:xxx/api/Test -> leads to the Get method of my TestController inheriting from ApiController.
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.
My issue was related to returning the entity model instead of the model I exposed (builder.EntitySet<ProductModel>("Products");). Solution was to map entity to resource model.
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
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));
None of the excellent solutions on this page worked for me. By debugging, I could see that the route was getting picked up and the OData queries were running correctly. However, they were getting mangled after the controller had exited, which suggested that it was the formatting that was generating what appears to be the OData catch-all error: 406 Not Acceptable.
I fixed this by adding a custom formatter based on the Json.NET library:
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}));
}
}
Then in WebApiConfig.cs, I added the line config.Formatters.Insert(0, new JsonDotNetFormatter()). Note that I am sticking closely to the order described in Jerther's answer.
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());
}
}
The problem/solution in my case was even more stupid. I'd left test code in my action that returned a completely different model type, just a Dictionary, and not my proper EDM model type.
Though I protest that the use of HTTP 406 Not Acceptable to communicate the error of my ways, is equally as stupid.
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.
Found in the GitHub error: "Unable to use odata $select, $expand, and others by default #511", their solution is to put the following line BEFORE registering the route:
// enable query options for all properties
config.Filter().Expand().Select().OrderBy().MaxTop(null).Count();
Worked like a charm for me.
Source: https://github.com/OData/RESTier/issues/511
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; }
In my case (odata V3) I had to change name of OdataController to be same as provided in
ODataConventionModelBuilder and that solved the issue
my controller:
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);
});
}
}
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);