I am working on a new asp.net web api restful service and spent some time with some Pluralsight courses on the subject. One of the better ones dives deep into design and the implementation of hypermedia (HATEOAS).
I followed the implementation in the video as it was very straight forward and being new to mvc/web api it was really helpful to see it working end to end.
However as soon as I started to dig a bit deeper into my implementation, the use of a UrlHelper() to calculate the link to return began to fall apart.
In the code below, I have a simple Get() which returns a collection of a particular resources and then a Get(int id) which allows for the returning of a individual resource.
All of the results go through a ModelFactory which transforms my POCOs to return results and back again on post, patch and puts.
I was trying to do this in a more sophisticated way by allowing the ModelFactory to handle all of the intelligence of link creation since it is constructed using the Request object.
Now I know I could solve all of this by simply handling the link generation/inclusion right inside my methods and maybe that is the answer but I was curious how others are handling it.
My goal:
1) In result sets (i.e. collections of results returned by "Get()"), to include total item count, total page count, next and previous pages as necessary. I have implemented a custom json converter to drop empty links on the ground. For example, I do not print out "prevPage" when you are on the first page. This works today.
2) In individual results (i.e. result returned by "Get(id)"), to include links to self, include rel, method the link represents and whether or not it is templated. This works today.
What is broken:
As you will see in the output below, two things are "wrong". When you look at the "POST" link for a new individual item, the URL is correct. This is because I am stripping out the last portion of the URI (dropping the resource ID). When returning a result set however, the URI for a "POST" is now incorrect. This is because the route did not include the individual resource id since "Get()" was called, not "Get(id)".
Again, the implementation could be changed to produce different links depending on which method was hit, pulling them out of the factory and into controller but I would like to believe I am just missing something obvious.
Any pointers for this newbie to routing and Web API?
Controller Get()
[HttpGet]
public IHttpActionResult Get(int pageSize = 50, int page = 0)
{
if (pageSize == 0)
{
pageSize = 50;
}
var links = new List<LinkModel>();
var baseQuery = _deliverableService.Query().Select();
var totalCount = baseQuery.Count();
var totalPages = Math.Ceiling((double) totalCount / pageSize);
var helper = new UrlHelper(Request);
if (page > 0)
{
links.Add(TheModelFactory.CreateLink(helper.Link("Deliverables",
new
{
pageSize,
page = page - 1
}),
"prevPage"));
}
if (page < totalPages - 1)
{
links.Add(TheModelFactory.CreateLink(helper.Link("Deliverables",
new
{
pageSize,
page = page + 1
}),
"nextPage"));
}
var results = baseQuery
.Skip(page * pageSize)
.Take(pageSize)
.Select(p => TheModelFactory.Create(p))
.ToList();
return Ok(new DeliverableResultSet
{
TotalCount = totalCount,
TotalPages = totalPages,
Links = links,
Results = results
}
);
}
Controller Get(id)
[HttpGet]
public IHttpActionResult GetById(int id)
{
var entity = _deliverableService.Find(id);
if (entity == null)
{
return NotFound();
}
return Ok(TheModelFactory.Create(entity));
}
ModelFactory Create()
public DeliverableModel Create(Deliverable deliverable)
{
return new DeliverableModel
{
Links = new List<LinkModel>
{
CreateLink(_urlHelper.Link("deliverables",
new
{
id = deliverable.Id
}),
"self"),
CreateLink(_urlHelper.Link("deliverables",
new
{
id = deliverable.Id
}),
"update", "PUT"),
CreateLink(_urlHelper.Link("deliverables",
new
{
id = deliverable.Id
}),
"delete", "DELETE"),
CreateLink(GetParentUri() , "new", "POST")
},
Description = deliverable.Description,
Name = deliverable.Name,
Id = deliverable.Id
};
}
ModelFactory CreateLink()
public LinkModel CreateLink(string href, string rel, string method = "GET", bool isTemplated = false)
{
return new LinkModel
{
Href = href,
Rel = rel,
Method = method,
IsTemplated = isTemplated
};
}
Result of Get()
{
totalCount: 10,
totalPages: 4,
links: [{
href: "https://localhost/Test.API/api/deliverables?pageSize=2&page=1",
rel: "nextPage"
}],
results: [{
links: [{
href: "https://localhost/Test.API/api/deliverables/2",
rel: "self"
},
{
href: "https://localhost/Test.API/api/deliverables/2",
rel: "update",
method: "PUT"
},
{
href: "https://localhost/Test.API/api/deliverables/2",
rel: "delete",
method: "DELETE"
},
{
href: "https://localhost/Test.API/api/",
rel: "new",
method: "POST"
}],
name: "Deliverable1",
description: "",
id: 2
},
{
links: [{
href: "https://localhost/Test.API/api/deliverables/3",
rel: "self"
},
{
href: "https://localhost/Test.API/api/deliverables/3",
rel: "update",
method: "PUT"
},
{
href: "https://localhost/Test.API/api/deliverables/3",
rel: "delete",
method: "DELETE"
},
{
href: "https://localhost/Test.API/api/",
rel: "new",
method: "POST"
}],
name: "Deliverable2",
description: "",
id: 3
}]
}
Result of Get(id)
{
links: [{
href: "https://localhost/Test.API/api/deliverables/2",
rel: "self"
},
{
href: "https://localhost/Test.API/api/deliverables/2",
rel: "update",
method: "PUT"
},
{
href: "https://localhost/Test.API/api/deliverables/2",
rel: "delete",
method: "DELETE"
},
{
href: "https://localhost/Test.API/api/deliverables/",
rel: "new",
method: "POST"
}],
name: "Deliverable2",
description: "",
id: 2
}
Update 1
On Friday I found and began to implement the solution outlined here: http://benfoster.io/blog/generating-hypermedia-links-in-aspnet-web-api. Ben's solution is very well thought out and allows me to maintain my models (stored in a publicly available library for use in other .NET (i.e. RestSharp)) solutions and allows me to use AutoMapper instead of implementing my own ModelFactory. Where AutoMapper fell short was when I needed to work with contextual data (such as the Request). Since my HATEOAS implementation has been pulled out and into a MessageHandler, AutoMapper again becomes a viable option.
I extended Ben's solution (link below) and it has met every requirement I have placed on it. I believe that "enriching" the return result in the handlers with the required HATEOAS data is the way to go. The only time I need to set links directly outside of the handler is when I get into things like paging where only the controller has the necessary information to make the decision on what the links should look like. At that point, I simply add the link to the collection on my model which carries through to the handler where even more links might be added.
http://benfoster.io/blog/generating-hypermedia-links-in-aspnet-web-api
I have extended Ben's approach using ASP.NET Core. My approach uses a ResultFilter where the response is decorated with links. A link builder(enricher) is used for
every Model that supports Hypermedia links. Because there is not a official standard on how the links will be formatted, the Paypal's definitions are used.
Please check my blog Generating Hypermedia links for an ASP.NET Core Web API
Related
I have been working on a MVC C# project and need to load a jQuery datatable from a specific db' table, the datatable must show the records of a specific region (and the regions are integers for example 01, 02, 03...) and no more, what I want to know is how the send an integer (that represents a specific region) to the controller from the DataTable script code.
this is the code of my datatable
$('#myTable').DataTable({
searching: false,
paging: true,
responsive: true,
ordering: false,
bInfo: false,
bLengthChange: false,
processing: true,
info: false,
deferRender: true,
orderMulti: false,
"ajax": {
"url": "../home/CargarTabla",
"type": "GET",
"datatype": "json"
},
"columns": [ ... all my columns
and this is the code of my MVC controller, it calls a WCF Service to get the info and just for debug I've specified a int value(example=28) and send the value in (info = cliente.CargarDatatable(ejemplo)) as I show in the code below
public ActionResult CargarTabla()
{
int example= 28;
ProxyGFC.ServiceGFCClient cliente = new ProxyGFC.ServiceGFCClient();
List<WcfService.Entidades.EmpleadoDatatable> info = new List<WcfService.Entidades.EmpleadoDatatable>();
info = cliente.CargarDatatable(ejemplo);
// a lot of boring code here...but it works
return Json(new { data = lista }, JsonRequestBehavior.AllowGet);
}
How do I send the integer value from my View to the controller ?
Define a parameter in your Controller Action and pass that parameter into your method that populates your value:
// The id value will read any querystring values or those found in the request
// as set them to the expected value
public ActionResult CargarTabla(int id)
{
// Use your id here to pass along to your service
}
Then simply adjust your client-side call to pass it along either as a querystring parameter (e.g. "url": "../home/CargarTabla?id=42"). You could do this dynamically as well by referencing an HTML element and passing its value along:
var id = $('selector').val();
$('#myTable').DataTable({
// Omitted for brevity
"ajax": {
"url": "../home/CargarTabla?id=" + id,
"type": "GET",
"datatype": "json",
// Another option may be to use a data object instead of a querystring param.
data { id: id }
}
});
I am making good progress using a Google Map.
The example I am following uses a hard coded JSON array to create annotations
// You can make markers different colors... google it up!
marker.setIcon('http://maps.google.com/mapfiles/ms/icons/green-dot.png')
// a sample list of JSON encoded data of places to visit in Liverpool, UK
// you can either make up a JSON list server side, or call it from a controller using JSONResult
var data = [
{ "Id": 1, "PlaceName": "Liverpool Museum", "OpeningHours":"9-5, M-F","GeoLong": "53.410146", "GeoLat": "-2.979919" },
{ "Id": 2, "PlaceName": "Merseyside Maritime Museum ", "OpeningHours": "9-1,2-5, M-F", "GeoLong": "53.401217", "GeoLat": "-2.993052" },
{ "Id": 3, "PlaceName": "Walker Art Gallery", "OpeningHours": "9-7, M-F", "GeoLong": "53.409839", "GeoLat": "-2.979447" },
{ "Id": 4, "PlaceName": "National Conservation Centre", "OpeningHours": "10-6, M-F", "GeoLong": "53.407511", "GeoLat": "-2.984683" }
];
// Using the JQuery "each" selector to iterate through the JSON list and drop marker pins
$.each(data, function (i, item) {
var marker = new google.maps.Marker({
'position': new google.maps.LatLng(item.GeoLong, item.GeoLat),
'map': map,
'title': item.PlaceName
});
However, I am new to MVC. Please can someone clarify the 2 approaches mentioned in the comment? I have all the points in a list of C# objects. This list is in my model object for the page. AlertsDashboardModel which contains a list - Alerts
I would probably use the list in the model directly, but I am curious how the other approach would work
I am using MVC 5 with Razor
Paul
The second approach is alluding to loading the data once the page has loaded (or as a result of a user interaction on the page like pressing a button) which then sends an ajax request to the server which returns the data:
Client:
$(function () {
//fetch data points from server on page load
$.get("/YourController/GetDataPoints", function(data) {
$.each(data, function (i, item) {
//rest omitted
});
}
Server: (in your Controller)
public JsonResult GetDataPoints()
{
return new JsonResult { Data = ...your list here };
}
UPDATE
If you want to do just the first method of embedded the list as javascript on the server side do this in your razor view:
<script>
var data = #Html.Raw(JsonConvert.SerializeObject(Model.DataPoints));
</script>
Regarding security you need to ensure that everything in that list comes from a trusted source and there is no unvalidated strings that could get injected into the page (if your data point class contains just int you will be fine)
I have looked around, but have not found anything (Angular post) that can actually make a successful call to a MVC Controller. I know there are a lot of Angular/.Net devs out there. Can I get some help?
Let's keep answers bare bones simple!!!
If I set a linebreak on the controller, I can see that this code is not actually hitting the controller.
HTML
<!-- I click this button -->
<input type="button" value="click" onclick="postit()" />
Javascript/Angular Post
function postit() {
$http({
method: 'POST',
url: 'Home/Give/',
data: { id: 4 }
}).success(successFn).error(errorFn);
}
function successFn() {
alert("success");
}
MVC C# Controller
[AcceptVerbs("OPTIONS")]
public ActionResult Give(int id)
{
var response = "some response" + id.ToString();
return Json(new JavaScriptSerializer().Serialize(response));
}
king Puppy, I've seen a few responses that dictate that the controller parameters should be an object that matches the object that is being sent, however, it seems that it's a little more forgiving than that. Consider the following example (I've updated your function a little):
Javascript:
$scope.postIt = function() {
var data = {
id = 4
};
$http
.post('Home/Give', data)
.success(function(data, status, headers, config) {
successFn();
})
.errors(function(data, status, headers, config) {
errorFn();
});
};
function successFn() {
alert("success");
};
function errorFn() {
alert("error");
};
MVC:
public ActionResult Give(int id)
{
var response = "some response" + id.ToString();
return Json(new JavaScriptSerializer().Serialize(response));
}
If you set a breakpoint, you will see that the id passed in is 4.
If you needed to pass in an object (so more than just one id), you could either create a matching class or struct on the controller side, or have multiple parameters (provided that they are simple value types)
ie:
public JsonResult Give (int id, int reasonId)
{
...
}
Anyway, I realize the post is old, but perhaps it will help you or others.
#kingPuppy this is my way to how to make angularjs post to mvc controller
first, html button for passing the angular js button click function;
<button class="btn btn-info" id="runButton" ng-click="runService('Hi')">Run</button>
so runService angular click (ng-click) function;
// Operation Type is my mvc controller's param
$scope.runService = function (optionType) {
$http({
url: '/System/RunService',
method: "POST",
data: {operationType : optionType}
}).then(function onSuccess(response) {
// Handle success
console.log(response);
}).catch(function onError(response) {
// Handle error
console.log(response);
});
}
And finally this is system controller's action;
NOT : Dont forget to [HttpPost] attribute
[HttpPost]
public ActionResult RunService(string operationType)
{
// Codes
Response.StatusCode = 200;
return Json(JsonRequestBehavior.AllowGet);
}
Hope this could help to you for how to make angular post to mvc controller. Thanks.
There is nothing special you have to do to get Angular to post to a standard MVC controller, and in fact I have several Angular/MVC apps that are using code almost identical to what you have above to POST to controllers that work fine.
I would use Firebug to confirm that your app is posting to the right place. One thing I noticed is that you might not want that trailing / at the end of your URL (so Home/Give instead of Home/Give/)
Good luck!
I have a Web API "ApiController" that I would like to get Kendo to do server side paging.
This is not MVC this is Web API v2.
The controller is returning json data to the caller.
The kendo grid works fine when serverPaging: = false.
The kendo grid does work when the serverPaging: = true but it does not show the correct total number of items and it does not have the correct number of pages displayed at the bottom of the grid.
I used this as the example http://bitoftech.net/2013/11/25/implement-resources-pagination-asp-net-web-api/ but the Get method with the page / page size parameters never gets called.
The Get method without parameters does get called but the request url looks like the following. localhost:9000/api/products/?{"take":2,"skip":0,"page":1,"pageSize":2}
My best guess is that the response needs something to tell the grid the number of records left but I can't find any good examples on how to tell the grid the total number of records.
I did add "X-Pagination" to the header with the total count and total pages but I don't think the kendo grid is using the information.
P.S.
I know that Telerik has code for paging with Web API but I can't afford / too cheap for the new Kendo so I'm using the open source kendo.
EDIT: 4-11-14
So I finally got my original problem solved where the total number of items was not showing. While still using WEB API 2 and JSON. "NOT ODATA! I'm still working on the ODATA test but that is for another post ;)".
basically in my GET method in the controller I had to return the following:
return new
{
TotalCount = totalCount,
TotalPages = totalPages,
Results = results
};
This is based on the example posted above. But in the kendo datasource schema I added the following:
schema: {
data: function (data) {
return data.Results || data;
},
total: function (data) {
if (data.TotalCount) {
return data.TotalCount;
}
return result.PageSize || result.length || 0;
},
I never did get the Get(int page = 0, int pageSize = 10) from the example to work but I was able to parse the query string to pull out the "skip" and "take" for paging to work.
In order to get the total records to come down correctly, you have to pass an addition query $inlinecount=allpages. I found useful information at http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options. Specifically, I used the solution where the Get method in your controller looks something like:
public PageResult<Product> Get(ODataQueryOptions<Product> options)
{
IQueryable results = options.ApplyTo(_products.AsQueryable());
return new PageResult<Product>(
results as IEnumerable<Product>,
Request.GetNextPageLink(),
Request.GetInlineCount());
}
That makes the result look something like:
{
"Items": [
{ "ID":1,"Name":"Hat","Price":"15","Category":"Apparel" },
{ "ID":2,"Name":"Socks","Price":"5","Category":"Apparel" },
// Others not shown
],
"NextPageLink": "http://localhost/api/values?$inlinecount=allpages&$skip=10",
"Count": 50
}
There are a couple things you then need to do in your kendo datasource.
var source = new kendo.data.DataSource({
type: "odata",
schema: {
data: function (data) {
if (data.Items) {
return data.Items;
}
return [data];
},
total: function (data) {
return data.Count;
}
}});
I removed a lot of configuration options for brevity, but hopefully that pulls enough details together to be helpful.
I have created a C# asp.net MVC application. The controller is displayed below. I am using high charts and i want to populate the chart with what is returned by the controller.
There are 2 fields below, Mon and Tue, and i need to populate the hard coded values in the javascript below to display the values returned by the controller;
Note: I am not sure if the controller method works :( (I'm a beginner), but i am more concerned about how to populate the chart with the values returned by the controller.
My C# controller;
public string timeHour()
{
var m = new MyModel();
m.theTime = getAllTime(); // get all time
return new JavaScriptSerializer().Serialize(m);
}
The High Chart that i have it in the view;
$(function () {
$('#container').highcharts({
chart: {
type: 'areaspline'
},
title: {
text: 'some title'
},
legend: {
layout: 'vertical',
align: 'left',
verticalAlign: 'top',
x: 150,
y: 100,
borderWidth: 1,
backgroundColor: '#FFFFFF'
},
xAxis: {
categories: [
'Mon',
'Tue'
],
plotBands: [{ // visualize the weekend
from: 4.5,
to: 6.5,
color: 'rgba(68, 170, 213, .2)'
}]
},
plotOptions: {
areaspline: {
fillOpacity: 0.5
}
},
series: [{
name: 'John',
data: [3, 4]
}]
});
});
First off, I would change the Controller's return type from string to JsonResult. To have your Controllers return ActionResults is a good MVC-style convention, and it makes your code a little more descriptive.
[HttpGet]
public JsonResult timeHour()
{
var m = new MyModel();
m.theTime = getAllTime(); // get all time
return Json(m, JsonRequestBehavior.AllowGet)
}
I believe that GET and POST requests should both work for you. It sort of looks like getallTime() is idempotent (although the code isn't shown here), so it looks like a GET request should work. This can be done in jQuery using the .get() method:
$(function () {
$.get("timeHour", null, function(result) {
console.log("If the GET request is successful, the Controller will return the HighCharts data:");
console.log(result);
}, "json");
});
You're going to need to make sure that the JSON returned to the client is in a format that Highcharts likes. Now that the Controller is successfully called (via an AJAX GET request) and the Highcharts data is returned, your second question can be answered:
how to populate the chart with the values returned by the controller
... via the Highcharts "series.data" documentation. If you read this, I think you can figure out how you'd need to change the MyModel object to suit your needs.