Asp.Net WebApi OData with DataJs throws an error - asp.net-web-api

I just created a really simple ASP.Net WebApi project. I used NuGet to download the latest OData in WebAPI – RC release. I also download DataJs and Knockout via NuGet. All my dependencies are up to date. I created a simple "Books" class and wired everything together using HttpConfiguration.EnableOData(IEdmModel). I also added the [Queryable] attribute to my Get action in the controller. There is not database involved, I hard-coded the data I want returned. Basically, I did the minimum amount of changes to run my project with WebApi and OData.
When I try to query the OData service using DataJs, I get a 500 Internal Server Error in the response, but if I browse to the URL directly I can see the XML data. I've included the request, response, my C# class, the Javascript code, and the Global.asax code. What am I missing to get this to work?
REQUEST
Response Headers
Cache-Control private
Content-Length 966
Content-Type application/json; odata=fullmetadata; charset=utf-8
DataServiceVersion 3.0;
Date Fri, 21 Dec 2012 22:13:27 GMT
Server Microsoft-IIS/8.0
X-AspNet-Version 4.0.30319
X-Powered-By ASP.NET
X-SourceFiles =?UTF-8?B?YzpcdXNlcnNcanVzdGluXGRvY3VtZW50c1x2aXN1YWwgc3R1ZGlvIDIwMTJcUHJvamVjdHNcRGF0YUpzU3Bpa2VcRGF0YUpzU3Bpa2VcYXBpXEJvb2tz?=
Request Headers
Accept application/atomsvc+xml;q=0.8, application/json;odata=fullmetadata;q=0.7, application/json;q=0.5, */*;q=0.1
Accept-Encoding gzip, deflate
Accept-Language en-US,en;q=0.5
Connection keep-alive
Cookie glimpseState=null; glimpseLatestVersion=0.87; glimpseOptions=null; glimpseClientName=null
Host localhost:31652
MaxDataServiceVersion 3.0
Referer http://{localhost}/
User-Agent Mozilla/5.0 (Windows NT 6.2; WOW64; rv:17.0) Gecko/20100101 Firefox/17.0
RESPONSE
{
"odata.error":{
"code":"","message":{
"lang":"en-US","value":"An error has occurred."
},"innererror":{
"message":"The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; odata=fullmetadata; charset=utf-8'.","type":"System.InvalidOperationException","stacktrace":"","internalexception":{
"message":"The related entity set could not be found. The related entity set is required to serialize the payload.","type":"System.Runtime.Serialization.SerializationException","stacktrace":" at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObject(Object graph, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)\r\n at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.<>c__DisplayClass8.<WriteToStreamAsync>b__7()\r\n at System.Threading.Tasks.TaskHelpers.RunSynchronously(Action action, CancellationToken token)"
}
}
}
}
C# Class
namespace DataJsSpike.Models
{
public class Book
{
public string ISBN { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public string Publisher { get; set; }
}
}
Javascript Code
// the URL of the first page to retrieve
var startPage = "api/Books";
var viewModel = new Object();
viewModel.books = ko.observable();
// On initialization, make a request for the first page
$(document).ready(function () {
LoadDataJs();
function LoadDataJs() {
OData.read(startPage, function (data) {
viewModel.books(data.results);
ko.applyBindings(viewModel);
});
}
});
Global.asax
public class WebApiApplication : HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
var modelBuilder = new ODataConventionModelBuilder();
EntityTypeConfiguration<Book> bookConfiguration = modelBuilder.Entity<Book>();
bookConfiguration.HasKey(x => x.ISBN);
modelBuilder.EntitySet<Book>("Books");
IEdmModel model = modelBuilder.GetEdmModel();
GlobalConfiguration.Configuration.EnableOData(model, "api");
}
}

EnableOData actually registers a route for you, but since you registered routes before it ran, those routes take precedence. If you remove this line:
RouteConfig.RegisterRoutes(RouteTable.Routes);
I think it should work out. The request needs to come in on an OData route for the OData formatting to work because the route parses the OData path and gives the formatter information about things like the Entity Set that's being accessed.

Related

Passing Odata Query Options in the Request Body

In the Odata 4.01 URL conventions it says that for GET requests with extremely long filter expressions you can append /$query to the resource path of the URL, use the POST verb instead of GET, and pass the query options part of the URL in the request body. If I try that with my service I get back a 404.
Does the /$query endpoint need to be manually created in the back end or is this something odata is supposed to take care of transparently? I've been searching like crazy but I'm having trouble finding anything about how to implement this.
To support this you add app.UseODataQueryRequest() to your startup somewhere before app.UseRouting()
The framework then transforms ~/$query POST requests into GET requests which are handled by the HttpGet action methods on your controller (source).
Documentation is here (although currently not up to date)
For a complete sample have a look here
One way to avoid this is wrapping the request in a batch request
You can resolve long url with $batch query.
Good post from Hassan Habib https://devblogs.microsoft.com/odata/all-in-one-with-odata-batch//
All what you should do is:
Allow batching in Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseODataBatching(); <---- (1)
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.Select().Filter().Expand().OrderBy();
endpoints.MapODataRoute(
routeName: "api",
routePrefix: "api",
model: GetEdmModel(),
batchHandler: new DefaultODataBatchHandler()); <---- (2)
});
}
Request batch query with body, that contains long url request
POST http://localhost/api/$batch
Content-Type: multipart/mixed; boundary=batch_mybatch
body:
--batch_mybatch
Content-Type: application/http
Content-Transfer-Encoding: binary
GET http://localhost/api/students/735b6ae6-485e-4ad8-a993-36227ac82851 HTTP/1.1 <--long url requst
OData-Version: 4.0
OData-MaxVersion: 4.0
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
User-Agent: Microsoft ADO.NET Data Services
--batch_mybatch

Angular POST request's header is null

I am developing an Ionic 3 Mobile Application, I have problem with Angular's POST method.
In login page, I created a form and tried send data to server with Angular HTTP POST method. But in server (.NET WEB API) I see request's header is null.
Here is the Angular side codes;
login(username, password):Observable<Object>{
let url : string = this.apiUrl+"/login";
let headers = new HttpHeaders();
headers.append('Authorization', btoa(username+":"+password).toString());
return this.http.post(url,JSON.stringify({username,password}), {headers: headers});
}
Here is the .NET side codes for controller;
[EnableCors(origins: "http://localhost:8100", headers: "*", methods: "*")]
public Response Post()
{
return _mobileUserService.Login();
}
Here is the part of .NET side codes for catch request;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
try
{
var token = request.Headers.GetValues("Authorization").FirstOrDefault();
}
catch (Exception ex)
{
}
return base.SendAsync(request, cancellationToken);
}
When request catched by .NET (in running), I see these values for "request" variable;
request = {Method: POST, RequestUri: 'http://localhost:41582/api/login', Version: 1.1, Content: System.Web.Http.WebHost.HttpControllerHandler+LazyStreamContent, Headers:
{
Connection: keep-alive
Accept: application/json
Accept: text/plain
Accept: */*
...
In normally, request's url is localhost:8100, so I think server accepted CORS
How can I solve that?
In Web api you have to tell which method is post or get based on how you have setup your route.
[EnableCors(origins: "http://localhost:8100", headers: "*", methods: "*")]
[HttpPost] // Decorate post this attribute in your controller
public Response Post()
{
return _mobileUserService.Login();
}

WebAPI OData v4 custom action without parameters can't be routed with error "No routing convention was found..."

I have very simple OData controller that successfully process standard actions (at least GET, POST, PUT and DELETE methods are working). I have followed this tutorial and added simple bound action. The method has parameters argument, but actually it does not required the parameters:
[HttpPost]
public IHttpActionResult Close([FromODataUri] int key, ODataActionParameters parameters) {
return Ok();
}
I have defined this action in OData EDM configuration as following:
builder.EntitySet<Ticket>("tickets");
builder.EntityType<Ticket>().Action("Close");
I am trying to call action from Postman:
POST /odata/tickets(2)/Default.Close HTTP/1.1
Host: localhost:50477
Accept: application/json
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: eef4c1f6-8c7f-f5eb-c22d-4397f3bda170
But receives the error message:
{
"error": {
"code": "",
"message": "No HTTP resource was found that matches the request URI 'http://localhost:50477/odata/tickets(2)/default.close'.",
"innererror": {
"message": "No routing convention was found to select an action for the OData path with template '~/entityset/key/unresolved'.",
"type": "",
"stacktrace": ""
}
}
}
I have read the whole internet and all related articles on SO but can't fix this issue. Please help me because I have no any fresh idea how to fight this.
My controller:
public class TicketsController : ODataController
{
[HttpPost]
public IHttpActionResult Close([FromODataUri] int key, ODataActionParameters parameters)
{
return Ok();
}
}
My request:
string requestUri = "http://localhost/odata/tickets(2)/Default.Close";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri);
request.Content = new StringContent("",
Encoding.UTF8,
"application/json");
HttpResponseMessage response = _client.SendAsync(request).Result;
Or remove the ODataActionParameters parameters in the close method and call with:
string requestUri = "http://localhost/odata/tickets(2)/Default.Close";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri);
HttpResponseMessage response = _client.SendAsync(request).Result;
My EdmModel is use your model.

.Net properties are missing in the response got from AngularJs Ajax call to Asp.Net Web Api

I am making an ajax call using AngularJS $http to Asp.Net Web Api.
$http({
method: 'GET',
url: '/api/PatientCategoryApi/PatCat',
params: dataTemp,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}).then(function successCallback(response) {
$scope.modelObject.resultData = response.data;
alert(JSON.stringify(response.data));
}, function errorCallback(response) {
$scope.modelObject.result = response;
});
The web api action has the following signature
public PaginatedList<PatientCategory> PatCat(PaginatedRequestCommand cmd){...}
The PaginatedList type is as follows.
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public int TotalPageCount { get; private set; }
public bool HasPreviousPage
{
get
{
return (PageIndex > 1);
}
}
public bool HasNextPage
{
get
{
return (PageIndex < TotalPageCount);
}
}
}
I have removed the ctor as its not required here. So its just a wrapper to a list of T and in this case T is PatientCategory.
I am able to access the list from the response object after the ajax call. But I am not able to find the properties of the PaginatedList such as
PageIndex, PageSize and so on in the response object. Even as I use fiddler, I can clearly see the array of PatientCategory objects but as the properties of the PaginatedList are missing.
[{"Id":1,"Code":"H05120000003","Name":"Regular","Description":"The Regular Patient Category","ModifiedTime":"2015-10-01T19:34:33.727","History":null,"MyXmlColumn":null},{"Id":2,"Code":"BH130000001","Name":"STAFF_CHGD_TO_REGULAR","Description":null,"ModifiedTime":"2015-10-01T19:46:07.093","History":null,"MyXmlColumn":null},{"Id":3,"Code":"BH130000001","Name":"STAFF_CHGD_TO_REGULAR","Description":null,"ModifiedTime":"2015-10-01T19:52:03.773","History":null,"MyXmlColumn":null}]
So how can I access those properties as well in the response?
The request and response that show up in Fiddler are as follows.
Request
GET http://localhost:50849/api/PatientCategoryApi/PatCat?Page=1&Take=3 HTTP/1.1
Host: localhost:50849
Connection: keep-alive
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36
Referer: http://localhost:50849/PatientCategory/Index
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
Response
HTTP/1.1 200 OK
Content-Length: 479
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
X-SourceFiles: =?UTF-8?B?RDpcQlZIXEF2YmhIaXNcQXZiaEhpcy5XZWJcYXBpXFBhdGllbnRDYXRlZ29yeUFwaVxQYXRDYXQ=?=
X-Powered-By: ASP.NET
Date: Thu, 15 Oct 2015 01:43:57 GMT
[{"Id":1,"Code":"H05120000003","Name":"Regular","Description":"The Regular Patient Category","ModifiedTime":"2015-10-01T19:34:33.727","History":null,"MyXmlColumn":null},{"Id":2,"Code":"BH130000001","Name":"STAFF_CHGD_TO_REGULAR","Description":null,"ModifiedTime":"2015-10-01T19:46:07.093","History":null,"MyXmlColumn":null},{"Id":3,"Code":"BH130000001","Name":"STAFF_CHGD_TO_REGULAR","Description":null,"ModifiedTime":"2015-10-01T19:52:03.773","History":null,"MyXmlColumn":null}]
Asp.net web api use NewtonJson.dll to make a Javascript serialization. When newtonjson find a class implement IEnumerable, it will serialize this object to an Javascript object like this [{},{}], it seems newtonjson only serialize the IEnumebrable object, other properties are ignored. Also you can add the NewtonSoft.Json.JsonObject to the class PagenatedList, this attribute will tell newtonjson serialize the data to a javascript plain object like this {p1: "", p2: ""}, but you will find the IEnumebrable data lost. To solve this, you should custom a new CustomJsonConverter inherited from JsonConverter, and add the attribute to your PagenatedList like this:
[Newtonsoft.Json.JsonConverter(typeof(CustomJsonConverter))
public class PagenatedList<T>:List<T>
//your class code

Self-hosted Web API - Post Param null

I'm trying to set up a simple test to make a POST request to my self-hosted Web-API server.
var config = new HttpSelfHostConfiguration(new Uri(baseAddress));
// add a route
config.Routes.MapHttpRoute(name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
server = new HttpSelfHostServer(config);
server.OpenAsync().Wait();
I added a Controller:
public class PayloadController : ApiController
{
public string Get()
{
return "Hello from GET";
}
public HttpResponseMessage PostItem(Payload payload)
{
// payload
//...
}
}
And I do have the corresponding model (making sure it contains properties):
public class Payload
{
public string Date { get; set; }
public string Command { get; set; }
}
I'm using Rest Console on Chrome to test the server, getting correct results for the GET in my controller.
Issueing the following request as POST:
Request Url: http://localhost:8080/api/payload
Request Method: POST
Params: {
"Date": "2012.09.26",
"Command": "Hello"
}
EDIT: Content-Type is set to application/json and encoding to UTF-8
Setting a break-point at the controller shows that the POST params are not correctly deserialized into an Paylod object, the param is null.
What am I possibly missing?
You need to specify the request Content-Type as application/json
Edit
Complete response with info from the comments below:
If you are using REST console's "request parameters" instead of "request payload raw body" it will not generate JSON out of them even when you specify application/json.
You need to paste raw JSON into the raw body field like this: http://screencast.com/t/iRWZqv91
{
"Date": "2012.09.26",
"Command": "Hello"
}
If you are receiving a null post parameter to your controller you need to create a constructor with 0 parameters in your model.
Greetings

Resources