ASP.net Web api endpoint not hitting - c#

I have created a endpoint in asp.net webapi that accepts two parameters. I am using postman to test it but the endpoint is not hitting. I have other methods that hit which dont have parameters passed in. Not sure why this is giving a problem.
I tried calling the endpoint using the following url in postman
http://localhost:56888/api/terms/reviewlegalfundclass/11166/2
I also changed the attribute route to
[Route("api/terms/{fundclassId}/{reviewtypeId}")]
What I have seen works is this combination
[Route("api/terms/reviewlegalfundclass/{id}")]
http://localhost:56888/api/terms/reviewlegalfundclass/11166 - postman call
Here is how the routing is configured in the webapi.config
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "WebApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
config.Formatters.Add(new BsonMediaTypeFormatter());
}
}
Controller
[System.Web.Http.HttpPut]
[SkipTokenAuthorization]
[Route("api/terms/reviewlegalfundclass/{fundClassId}/{reviewTypeId}")]
public string ReviewLegalFundClass(int fundClassId, int reviewTypeId)
{
var svc = Resolve<IUserIdentityService>();
var user = svc.GetCurrentUser();
try
{
var fcService = GetService<LEGAL_FUND_CLASS>();
var fundClass = fcService.Get(fundClassId);
var response = "N/A";
var users = GetViewService<V_PERMAL_USER_ALL>().GetAll();
if (reviewTypeId == (int)LegalFundClassReviewType.Fees)
{
fundClass.FEES_LAST_REVIEWED_BY_ID = user.ID;
fundClass.FEES_LAST_REVIEWED_DATE = DateTime.Now;
var feeUsr = users.FirstOrDefault(x => x.ID == fundClass.FEES_LAST_REVIEWED_BY_ID);
response = $"Last Reviewed: {feeUsr?.LOGIN_NAME} {fundClass.FEES_LAST_REVIEWED_DATE.Value.ToString(PermalConstants.DateFormat2)}";
}
else if (reviewTypeId == (int)LegalFundClassReviewType.Terms)
{
fundClass.TERMS_LAST_REVIEWED_BY_ID = user.ID;
fundClass.TERMS_LAST_REVIEWED_DATE = DateTime.Now;
var termsUsr = users.FirstOrDefault(x => x.ID == fundClass.TERMS_LAST_REVIEWED_BY_ID);
response = $"Last Reviewed: {termsUsr?.LOGIN_NAME} {fundClass.TERMS_LAST_REVIEWED_DATE.Value.ToString(PermalConstants.DateFormat2)}";
}
else throw new Exception("Invalid fund class review type");
fcService.Update(fundClass);
return response;
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}

Related

Swagger not displaying routes/requests

I have recently been attempting to add swagger to my project, ive followed all the guides but something seems to have gone wrong, ive used swagger for years but ive never had this issue before. It seems that its completely ignoring most of my routes and just using the PUT function, i cant explain it, but ive attached an image of my swagger and all the relevant code, i hope someone has experienced this befiore
Image of my swagger: https://prnt.sc/o7aIJEz0r7xc
Swaggerconfig:
public class SwaggerConfig
{
public static void Register(HttpConfiguration config)
{
config.EnableSwagger(c => {
c.SingleApiVersion("v1", "DefaultApi");
c.ApiKey("Token")
.Description("Bearer token")
.Name("Authorization")
.In("header");
}).EnableSwaggerUi(c =>
{
c.EnableApiKeySupport("Authorization", "header");
});
}
}
Httproute:
HttpConfiguration config = new HttpConfiguration();
config.EnableCors();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
SwaggerConfig.Register(config);
app.UseWebApi(config);
Controller:
{
[Route("api/[controller]")]
[EnableCors(origins: "*", headers: "*", methods: "*")]
public class AuthController : ApiController
{
public static DataContext DB = new DataContext();
//public static string test = "This is my current test key";
[HttpGet, Authorize(Roles = "Admin")]
[Route("GetMe")]
public IHttpActionResult GetMe()
{
var userName = User?.Identity?.Name;
var identity = (ClaimsIdentity)User.Identity;
IEnumerable<Claim> claims = identity.Claims;
var role = identity.FindFirst(ClaimTypes.Role).Value;
return Ok(new { userName, role, claims });
}
[HttpGet]
[Route("CorsTest")]
public IHttpActionResult CorsTest()
{
return Ok("xD");
}
//add role to user function
//Users (gets all users)
//
[HttpPost]
[Route("register")]
public IHttpActionResult Register(UserRegister request)
{
if (request.Username.Equals("") || request.Password.Equals(""))
{
return BadRequest("Plase input username and password");
}
string query = "Select * from JWTUSER Where userName= '" + request.Username + "'";
NpgsqlDataAdapter sda = new NpgsqlDataAdapter(query, DB.npgsqlConn);
DataTable dataTable = new DataTable();
sda.Fill(dataTable);
if (dataTable.Rows.Count >= 1)
{
return BadRequest("This username is already being used");
}
User newUser = new User();
CreatePasswordHash(request.Password, out byte[] passwordHash, out byte[] passwordSalt);
newUser.Username = request.Username;
newUser.Role = request.Role;
newUser.PasswordHash = passwordHash;
newUser.PasswordSalt = passwordSalt;
newUser.Email = request.Email;
newUser.PhoneNumber = request.PhoneNumber;
DB.RegisterUser(newUser);
return Ok(newUser);
}
[Authorize(Roles = "Admin")]
[HttpPut]
[Route("updateRole")]
public IHttpActionResult UpdateRole(UpdateRole request)
{
//string userId = SubjectId
//Replace body for register, instead of using username, extract username from database.
User newUser = new User();
if (request.Username.Equals(""))
{
return BadRequest("Plase input username");
}
string query = "Select * from JWTUSER Where userName= '" + request.Username + "'";
NpgsqlDataAdapter sda = new NpgsqlDataAdapter(query, DB.npgsqlConn);
DataTable dataTable = new DataTable();
sda.Fill(dataTable);
if (dataTable.Rows.Count > 1)
{
return BadRequest("Big error more then 1 user with this username");
}
if (dataTable.Rows.Count == 0)
{
return BadRequest("User not found");
}
foreach (var row in dataTable.AsEnumerable())
{
newUser.Username = row.Field<string>("userName");
newUser.Role = row.Field<string>("role");
newUser.PasswordHash = row.Field<byte[]>("passwordHash");
newUser.PasswordSalt = row.Field<byte[]>("passwordSalt");
}
newUser.Role = request.Role;
DB.UpdateRole(newUser);
return Ok("Role update to " + newUser.Role);
}
.net 6 swagger setup
**Program.cs**
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSwaggerGen(opt =>
{
opt.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description =
"JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345.54321\""
});
opt.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{ Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } },
new string[] { }
}
});
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
//Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
After a few hours of pain i realized that i had forgotten the most important part
config.MapHttpAttributeRoutes();
Thank you everyone for trying to help my broken mind <3.

Api controller post method returns 404 - not found

I have these controller methods in my web api, both of them accept HttpPost.
The Process() method receives a complex type parameter in the body.
The Install method receives a string parameter in the body.
The Process method is called successfully, but the Install method fails with error 404 - not found, I assume the routing is failing, but I just can't figure out what am I doing wrong...
[HttpPost]
[ResponseType(typeof(IProcessableObject))]
[Route("Workflow/Process")]
public IHttpActionResult Process([FromBody]SerializedObject request)
{
try
{
Type objectType = ResolveType(request.ObjectType);
IProcessableObject obj = (IProcessableObject)JsonSerializer.Deserialize(request.RawObject, objectType);
log.DebugFormat("Processing {0} with workflow {1}", objectType.Name, obj.WorkflowId);
var workflow = workflowController.Get(obj.WorkflowId, true);
var workflowProcessor = new WorkflowProcessor(obj, workflow);
if (workflowProcessor.Process())
return Ok(obj);
return InternalServerError();
}
catch (Exception ex)
{
log.Error(string.Format("Failed processing object {0}", request.ObjectType), ex);
return InternalServerError();
}
}
[HttpPost]
[ResponseType(typeof(int))]
[Route("Workflow/Install/{userName}")]
public IHttpActionResult Install(string userName, [FromBody]string xmlTemplate)
{
try
{
log.DebugFormat("User {0} is installing new workflow:{1}{2}", userName, Environment.NewLine, xmlTemplate);
var wf = workflowController.Install(xmlTemplate, userName);
if (wf == null)
return BadRequest();
return Ok(wf.WorkflowId);
}
catch (Exception ex)
{
log.Error("Failed installing workflow", ex);
return InternalServerError();
}
}
And from my MVC application I call them like this:
public static IProcessableObject Process(IProcessableObject obj, bool isProxy = false)
{
string requestURL = string.Concat(wfServiceUrl, "Workflow/Process");
var requestData = new SerializedObject
{
RawObject = JsonSerializer.Serialize(obj),
ObjectType = isProxy ? obj.GetType().BaseType.AssemblyQualifiedName : obj.GetType().AssemblyQualifiedName
};
using (var client = new WebClient())
{
client.Headers[HttpRequestHeader.ContentType] = "application/json";
var result = client.UploadString(requestURL, JsonSerializer.Serialize(requestData));
return (IProcessableObject)JsonSerializer.Deserialize(result, isProxy ? obj.GetType().BaseType : obj.GetType());
}
}
public static int Install(string workflowTemplate, string userName)
{
string requestURL = string.Concat(wfServiceUrl, "Workflow/Install/", userName);
using (var client = new WebClient())
{
client.Headers[HttpRequestHeader.ContentType] = "application/json";
var result = client.UploadString(requestURL, JsonSerializer.Serialize(workflowTemplate));
return JsonSerializer.Deserialize<int>(result);
}
}
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 }
);
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
}
}
Try changing this:
[Route("Workflow/Install/{userName}")]
For this:
[Route("api/Workflow/Install/{userName}")]
And do the same with your other routes, add api/ and that should work.

c# apicontroller gets called twice when it is called only once by getasync

Hello I have encountered very interesting bug, and can't understand why is this happening. I call GetAllDocuments() method to api from my other project via GetAsync method of httpclient. But the problem is GetAllDocuments returns and then it gets called again! GetAsync returns result after GetAllDocuments returns twice.
Here is the calling method :
public static async Task<Document> GetAllDocuments()
{
try
{
var response = _client.GetAsync("api/documents/GetAllDocuments").Result;
response.Content.LoadIntoBufferAsync().Wait();
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<Document>(new[] { new JsonMediaTypeFormatter() }); ;
// Return the URI of the created resource.
}
catch (Exception ex)
{
return null;
}
}
ApiController method :
[HttpGet]
public List<Document> GetAllDocuments()
{
lock (_lock)
{
_documentsRepository = DocumentsRepository.Instance;
var result = _documentsRepository.GetDocuments();
return result;
}
}
WebConfig :
public static class WebApiConfig
{
private static HttpSelfHostServer _server;
public static void Run(string port)
{
var config = new HttpSelfHostConfiguration($"http://localhost:{port}");//"http://localhost:8080");
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
config.Routes.MapHttpRoute(
"API Default", "api/{controller}/{id}",
new { id = RouteParameter.Optional });
config.Routes.MapHttpRoute(
"newdocument", "api/documents/newdocument/{document}",new { document = RouteParameter.Optional });
config.Routes.MapHttpRoute(
"GetAllDocuments", "api/documents/GetAllDocuments/");
config.MaxReceivedMessageSize = int.MaxValue;;
config.MaxBufferSize = int.MaxValue;
_server = new HttpSelfHostServer(config);
_server.OpenAsync();
}
public static void Stop()
{
_server?.CloseAsync();
}
}

Multiple types were found that match the controller

I'm trying to api versioning using header but in different folder structure like below.
In Controller folder have V1 sub folder inside that CustomerController.cs
and
In Controller folder have V2 sub folder inside that CustomerController.cs
when I user api version using URL above works fine.
my issue is When I try this approach with header it is giving me below error:
{ "Message": "An error has occurred.", "ExceptionMessage":
"Multiple types were found that match the controller named 'customer'.
This can happen if the route that services this request
('api/{controller}/{id}') found multiple controllers defined with the
same name but differing namespaces, which is not supported.\r\n\r\nThe
request for 'customer' has found the following matching
controllers:\r\nApiVersion.Controllers.V1.CustomerController\r\nApiVersion.Controllers.V2.CustomerController", "ExceptionType": "System.InvalidOperationException", "StackTrace": "
at
System.Web.Http.Dispatcher.DefaultHttpControllerSelector.SelectController(HttpRequestMessage
request)\r\n at
System.Web.Http.Dispatcher.HttpControllerDispatcher.d__1.MoveNext()"
}
WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
CustomeControllerSelector.cs
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
var controllers = GetControllerMapping();
var routeData = request.GetRouteData();
var controllerName = routeData.Values["controller"].ToString();
//HttpControllerDescriptor controllerDescriptor;
var version = GetVersionFromAcceptHeaderVersion(request);
var controllerKey = String.Format(CultureInfo.InvariantCulture, "{0}.{1}",
version, controllerName);
//if (controllers.TryGetValue(controllerName, out controllerDescriptor))
// {
if (!string.IsNullOrEmpty(version))
{
HttpControllerDescriptor versionedControllerDescriptor;
if (controllers.TryGetValue(controllerKey, out versionedControllerDescriptor))
{
return versionedControllerDescriptor;
}
}
return null;
}
private string GetVersionFromAcceptHeaderVersion(HttpRequestMessage request)
{
var acceptHeader = request.Headers.Accept;
foreach (var mime in acceptHeader)
{
if (mime.MediaType == "application/json" || mime.MediaType == "text/html")
{
var version = mime.Parameters
.Where(v => v.Name.Equals("version", StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
if (version != null)
{
return version.Value;
}
return string.Empty;
}
}
return string.Empty;
}
I tried following. Works as of now -
public class CustomControllerSelector : DefaultHttpControllerSelector
{
//const string partName = "Webapi.Controllers";
private readonly HttpConfiguration _config;
public CustomControllerSelector(HttpConfiguration config)
: base(config)
{
_config = config;
}
public override System.Web.Http.Controllers.HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
var _route = request.GetRouteData();
var controllerName = base.GetControllerName(request);
var type = _config.Services.GetAssembliesResolver();
var controlles = _config.Services.GetHttpControllerTypeResolver().GetControllerTypes(type);
object name;
_route.Values.TryGetValue("route", out name);
//No more hard coding
var partName = controllers.FirstOrDefault().Namespace;
var st = name as string;
if (st != null)
{
var conType = controlles.FirstOrDefault(a => a.Namespace == string.Format("{0}.{1}", partName, st));
if (conType != null)
return new System.Web.Http.Controllers.HttpControllerDescriptor(_config, controllerName, conType);
}
return base.SelectController(request);
}
}
In the WebApiConfig.cs -
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{route}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Test routes -
http://localhost:60957/api/Another/Route/a
http://localhost:60957/api/Another/Route2/aaaaa

WebAPI and ODataController return 406 Not Acceptable

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);

Categories

Resources