I am creating a REST API and I have been playing with the idea of allowing bundling of requests from clients. By bundling I mean they can send one request, containing multiple "real" requests, and they get delivered to the client together. Typically javascript ajax requests. Something like this:
POST /bundlerequest
["/person/3243", "/person/3243/friends", "/comments/3243?pagesize=10&page=1", "/products", "/product/categories" ]
(The bundled request can only be GET requests, as of now at least)
This is intended to return something like this
{
"success" : ["/person/3243", "/person/3243/friends", "/comments/3243?pagesize=10&page=1", "/products", "/product/categories" ],
"error" : [],
"completiontime" : 94,
other relevant metadata...
"responses" : [
{"key" : "/person/3243" , "data" : {"name" : "John", ...} },
{"key" : "/person/3243/friends" , "data" : [{"name": "Peter", "commonfriends" : 5, ...}] },
etc...
]
}
The benefits of this bundling is that it reduces the number of requests and that is especially important on mobile devices for instance.
So my first question is, is my approach to this a good one? Does anyone have experience with doing something like this?
AFAIK the common way of solving this is to write server side code to return combined data, that I believe is relevant for the client(s). (The twitter user stream for instance does this, combining person info, latest tweets, latest personal messages etc.) But this makes the API very opinionated and when the client needs changes the server might need to change to accomodate to optimize.
And the second question is how to implement this?
My backend is ASP.NET MVC 3 and IIS 7. Should I implement it in the application, having an bundlerequest action that internally calls the other actions specified in the request?
Could it be implemented in IIS 7 directly? Writing a module that transparently intercepts requests to /bundlerequest and then calls all the corresponding sub requests, making the application totally unaware of the bundling that happens? This would also allow me to implement this in an application-agnostic way.
You could use an asynchronous controller to aggregate those requests on the server. Let's first start by defining a view model that will be returned by the controller:
public class BundleRequest
{
public string[] Urls { get; set; }
}
public class BundleResponse
{
public IList<string> Success { get; set; }
public IList<string> Error { get; set; }
public IList<Response> Responses { get; set; }
}
public class Response
{
public string Key { get; set; }
public object Data { get; set; }
}
then the controller:
public class BundleController : AsyncController
{
public void IndexAsync(BundleRequest request)
{
AsyncManager.OutstandingOperations.Increment();
var tasks = request.Urls.Select(url =>
{
var r = WebRequest.Create(url);
return Task.Factory.FromAsync<WebResponse>(r.BeginGetResponse, r.EndGetResponse, url);
}).ToArray();
Task.Factory.ContinueWhenAll(tasks, completedTasks =>
{
var bundleResponse = new BundleResponse
{
Success = new List<string>(),
Error = new List<string>(),
Responses = new List<Response>()
};
foreach (var task in completedTasks)
{
var url = task.AsyncState as string;
if (task.Exception == null)
{
using (var response = task.Result)
using (var stream = response.GetResponseStream())
using (var reader = new StreamReader(stream))
{
bundleResponse.Success.Add(url);
bundleResponse.Responses.Add(new Response
{
Key = url,
Data = new JavaScriptSerializer().DeserializeObject(reader.ReadToEnd())
});
}
}
else
{
bundleResponse.Error.Add(url);
}
}
AsyncManager.Parameters["response"] = bundleResponse;
AsyncManager.OutstandingOperations.Decrement();
});
}
public ActionResult IndexCompleted(BundleResponse response)
{
return Json(response, JsonRequestBehavior.AllowGet);
}
}
and now we can invoke it:
var urls = [
'#Url.Action("index", "person", new { id = 3243 }, Request.Url.Scheme, Request.Url.Host)',
'#Url.Action("friends", "person", new { id = 3243 }, Request.Url.Scheme, Request.Url.Host)',
'#Url.Action("index", "comments", new { id = 3243, pagesize = 10, page = 1 }, Request.Url.Scheme, Request.Url.Host)',
'#Url.Action("index", "products", null, Request.Url.Scheme, Request.Url.Host)',
'#Url.Action("categories", "product", null, Request.Url.Scheme, Request.Url.Host)'
];
$.ajax({
url: '#Url.Action("Index", "Bundle")',
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(urls),
success: function(bundleResponse) {
// TODO: do something with the response
}
});
Of course some tweaking might be necessary to adapt this to your specific needs. For example you mentioned sending AJAX requests with session expired which might redirect to the Logon page and thus not capturing the error. That's indeed a PITA in ASP.NET. Phil Haack blogged a possible way to circumvent this undesired behavior in a RESTful manner. You just need to add a custom header to the requests.
I suggest looking wcf web api
Related
I am developing a REST API using Node.JS and AWS Lambda, which will be accessed by 3 apps.
Android app developed with Flutter
iOS app developed with Flutter
Web app developed with Java
I have always been a Java guy and never a Javascript guy. This is my first time on serious job with Javascript stuff.
Normally when we create REST APIs in Java, it will get the data from the database, convert it to a Java class and send it back as the response.
For an example, lets assume below is the structure of my database table. Have a close look at the names, AND the associated foreign keys.
In my Java based REST API, this will be the class.
public class SellerSettings implements java.io.Serializable {
private Integer idsellerSettings;
private User user;
private double responseRate;
private double responseTime;
private boolean isavailable;
public SellerSettings() {
}
public Integer getIdsellerSettings() {
return this.idsellerSettings;
}
public void setIdsellerSettings(Integer idsellerSettings) {
this.idsellerSettings = idsellerSettings;
}
public User getUser() {
return this.user;
}
public void setUser(User user) {
this.user = user;
}
public double getResponseRate() {
return this.responseRate;
}
public void setResponseRate(double responseRate) {
this.responseRate = responseRate;
}
public double getResponseTime() {
return this.responseTime;
}
public void setResponseTime(double responseTime) {
this.responseTime = responseTime;
}
public boolean getIsavailable() {
return this.isavailable;
}
public void setIsavailable(boolean isavailable) {
this.isavailable = isavailable;
}
}
When the data is requested from an API built with Java, it will send the response back with the same set of names you see in the java class I presented. Since the user is another class, it will actually add that's fields as well..
Anyway, this is my node.js code now.
const mysql = require('mysql');
const con = mysql.createConnection({
host : "*******.rds.amazonaws.com",
user : "****",
password : "*****",
port : 3306,
database : "*****"
});
exports.lambdahandler = (event, context, callback) => {
// allows for using callbacks as finish/error-handlers
context.callbackWaitsForEmptyEventLoop = false;
const sql = "select * from seller_settings";
con.query(sql, function (err, result) {
if (err) throw err;
var response = {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify(result),
"isBase64Encoded": false
};
callback(null, response)
});
};
And it return the below
[
{
"idseller_settings": 1,
"iduser": 1,
"response_rate": 90,
"response_time": 200,
"isavailable": 0
},
{
"idseller_settings": 2,
"iduser": 1,
"response_rate": null,
"response_time": 210,
"isavailable": 0
}
]
It is nothing but the pure table names.
As a Java guy, I looked at how we can convert these to the Java like names. Then I found Javascript classes. However I do understand that Javascript is not OOP as Java or anything similar. It is not perfect on OOP. Also adding these class, converting the values from JSON to classes, and the sending it back seems to be an over kill for my simple, nice Node.JS code.
As a Java guy, I would ask,
When requested via the REST API, Is it normal to grab the data from the database tables as per column names as it is and send it back in Javascript world? Or should I create classes?
My mobile and web apps are using classes in their languages, when sending POST requests they will most probably send a JSON that contains Java classe's variables I shared. From the node.js side, this can be converted to table names?
In my main.js file I want to have data from spring boot controller in some specific json format.
eg.
var contactsJSON = [{"userId": "firstuser", "displayName": "firstuser"},
{"userId": "seconduser", "displayName": "seconduser"}];
Now in my controller "/users" i'm returning list of all users.
I want that at the time of application loading the value of contactsJSON gets populated dynamically in required json format (I only need username to create JSON).
main.js
var contactsJSON = [{"userId": "firstuser", "displayName": "firstuser"
},
{"userId": "seconduser", "displayName": "seconduser"
},
{"userId": "thirduser", "displayName": "thirduser"
}
];
UserController.java
#RequestMapping(value = "/users", method = RequestMethod.GET)
public String viewUsers(Model model) {
List<User> list = userService.getAllUsers();
model.addAttribute("userList", list);
return "welcome";
}
List contains private Long id;
private String username;
private String password;
private String passwordConfirm;
I want to dynamically provide value of contactsJSON in my javascript file. How can I do this ?
You can either return a response as String or you can use ResponseEntity Object provided by Spring as below. By this way, you can also return Http status code which is more helpful in the web service call.
#RestController
#RequestMapping("/api")
public class MyRestController
{
#RequestMapping(value = "/users", method = RequestMethod.GET, produces=MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> viewUsers(Model model) {
{
//Get data from service layer into entityList.
List<User> list = userService.getAllUsers();
List<JSONObject> entities = new ArrayList<JSONObject>();
for (Entity n : list) { // You can iterate over the list and add in json format below is the example for same
//JSONObject entity = new JSONObject();
//entity.put("aa", "bb");
//entities.add(entity);
}
return new ResponseEntity<Object>(entities, HttpStatus.OK);
}
}
First, a Thymeleaf recommendation
I highly recommend Thymeleaf over JSP templates. For one thing, it makes inline object-to-JSON expressions very easy. For example...
<script th:inline="javascript">
const contactsJSON = [[${userList}]];
</script>
See https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#advanced-inlined-evaluation-and-javascript-serialization
If you don't want to switch, I would recommend adding an AJAX call to fetch your user data. On the Java side, it might look like this
#GetRequest(path = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public List<User> getAllUsers() {
return userService.getAllUsers();
}
Spring will automatically serialize the response as JSON.
Then in your welcome.jsp
<script>
// load the current URL (ie /users) but requesting JSON
fetch('', {
headers: {
Accept: 'application/json'
}
}).then(res => res.json()).then(contactsJSON => {
// you can now use contactsJSON here
})
</script>
I'm trying to post strings from an AngularJS application (using $http) to a server built on ASP.NET WebApi, but I get 404 as soon as I add a parameter.
The client code is this
$scope.add = function () {
// ...cut...
$http({ method: "POST", url: url, data: { fileString: "test string" }}).then(
function successCallback(response) {
$log.info(response.data);
}
);
}
The server code is
[HttpPost]
public IHttpActionResult UploadExcel(string fileString) {
// cut
}
I get a 404, but if I remove the parameter on server side it works, so i can use a server side code like this
[HttpPost]
public IHttpActionResult UploadExcel() {
// cut
}
What is wrong? Should I pass the data in a different way? I tried different combination but I can't get it work.
What you want to do is send a string, not a JSON object as you are doing right now with { fileString: "test string" }. When I want to send a string, what I normally do is that I send data from Angular like this:
$http.post("/Search/QuickSearch?searchQuery="+ searchString);
And my controller I make ready to receive a string like this:
[HttpPost]
public IHttpActionResult QuickSearch(string searchQuery)
{
// cut
}
If I want to send a JSON object, I tell my controller what it should expect, like this:
[HttpPost]
public IHttpActionResult SaveActivity(ActivityEditForm form);
{
// cut
}
public class ActivityEditForm
{
public int? Id { get; set; }
[Required]
public string Title { get; set; }
public string Description { get; set; }
}
And then send my JSON from Angular like this:
$http.post("/Activity/SaveActivity", { form: activity });
I suggest you should capture the request send by Angular. By default, Angular send parameters in a json string in request body.
I'm not sure wether Asp.net can parse them from json string in body.
So, you can try to add the below codes (also need jQuery)
angular.module('yourApp').config(function ($httpProvider) {
$httpProvider.defaults.transformRequest = function(data){
if (data === undefined) {
return data;
}
return $.param(data);
}
});
The first error is in the controller, [FromBody] should be used with the input parameter.
public IHttpActionResult UploadExcel([FromBody]string fileString)
Then the data variable on the client should be a single string, so
$http({ method: "POST", url: url, data: "test string" }).then(
Anyway I found some issue with this solution later, it seems the simplest but I suggest to avoid it.
Best solution
Thank to #Squazz answer and this SO answer I strongly suggest a change in the webapi controller, client was correct. Just introduce a class to handle a single string and adapt the input parameter
// new class with a single string
public class InputData {
public string fileString { get; set; }
}
// new controller
[HttpPost]
public IHttpActionResult UploadExcel([FromBody] InputData myInput) {
string fileString = myInput.fileString;
// cut
}
This way JSON code from the client is automatically parsed and it's easy to change the data input.
Extra tip
$scope.add angular function was correct as in the question, but here is a more complete example
$scope.testDelete = function () {
var url = "http://localhost/yourAppLink/yourControllerName/UploadExcel";
var data = ({ fileString: "yourStringHere" });
$http({ method: "POST", url: url, data: data }).then(
function successCallback(response) {
console.log("done, here is the answer: ", response.data);
}, function errorCallback(response) {
console.log("an error occurred");
}
);
}
I'm making an ASP.NET Web API web service, and an HTML/javascript page to test it. The issue I'm having is with passing a complex data parameter and having it come through properly in the web API controller.
I know there are numerous similar questions and I've read them and tried the solutions and haven't solved it. I have also read some JQuery documentation.
Here's my controller:
public class TitleEstimateController : ApiController
{
public IHttpActionResult GetTitleEstimate([FromUri] EstimateQuery query)
{
// All the values in "query" are null or zero
// Do some stuff with query if there were anything to do
}
}
public class EstimateQuery
{
// Various fields
}
The route mapping in WebApiConfig.cs:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{query}"
);
And the javascript:
var uri = 'api/titleEstimate/';
var query = {
"username": $("#user").val(),
// other fields
};
$.getJSON(uri,query)
.done(function (data) {
$('#product').text("OK");
})
.fail(function (jqXHR, textStatus, err) {
$('#product').text('Error: ' + err);
});
Currently I'm getting a 404. I tried $.getJSON(uri + '/' + query) but that didn't work either. Before I was passing this object I was calling it successfully so I think the routing is generally OK. I tried a type converter, but that didn't help, still a 404. Does anyone see what I'm missing/doing wrong?
Answer
You are using the wrong uri. You need api/titleEstimate/getTitleEstimate. That explains and will resolve your 404.
Answer to Follow Up Question
Everything else you're doing almost works. After you resolve the 404, you'll find that you aren't receiving the value FromUri, and you'll have a follow up question. So, you need to change your route config to this:
config.Routes.MapHttpRoute(
name: "Default",
routeTemplate: "api/{controller}/{action}"
);
Then you'll not only resolve your 404 but also receive the FromUri value that you're sending as query string parameters.
Demo
Here is a fiddle for evidence, which is calling into this controller, which is hosted LIVE here. The only thing I changed in the working version are the uri to resolve the 404 and the route config to make sure you receive the FromUri value. That's all.
HTML
<label>Username:
<input id="user" />
</label>
<button id="submit">Submit</button>
<button id="submit-fail">Submit Fail</button>
<div id="product"></div>
JavaScript
This will succeed.
Note, we only need domain because we doing cross-site scripting here for the demo purpose.
var domain = "https://cors-webapi.azurewebsites.net";
$("#submit").click(function () {
var uri = domain + '/api/titleEstimate/getTitleEstimate';
var query = {
"username": $("#user").val(),
// other fields
};
$.getJSON(uri, query)
.done(function (data) {
$('#product').text(data);
})
.fail(function (jqXHR, textStatus, err) {
$('#product').text('Error: ' + err);
});
});
This will 404.
$("#submit-fail").click(function () {
var uri = domain + '/api/titleEstimate';
var query = {
"username": $("#user").val(),
// other fields
};
$.getJSON(uri, query)
.done(function (data) {
$('#product').text(data);
})
.fail(function (jqXHR, textStatus, err) {
$('#product').text('Error: ' + err);
});
});
Controller
public class TitleEstimateController : ApiController
{
public class EstimateQuery
{
public string username { get; set; }
}
public IHttpActionResult GetTitleEstimate([FromUri] EstimateQuery query)
{
// All the values in "query" are null or zero
// Do some stuff with query if there were anything to do
if(query != null && query.username != null)
{
return Ok(query.username);
}
else
{
return Ok("Add a username!");
}
}
}
You can read more details about WebApi routing here. From reading it you can probably come up with an alternative solution within your route config. There are also lots of terrific examples in this blog post.
First, I would try to use the attribute routing feature of web.api like this:
[RoutePrefix("api/titleestimate")]
public class TitleEstimateController : ApiController
{
[HttpGet]
public IHttpActionResult GetTitleEstimate([FromUri] EstimateQuery query)
{
// All the values in "query" are null or zero
// Do some stuff with query if there were anything to do
}
}
It would also be helpful to see the request in your dev tools. I disagree with Colin that you should make this a POST, because HTTP POST is supposed to be used to create new items. You are trying to get information so HTTP GET makes sense.
I think Web.Api assumes methods are GETs by default, but declarating it with the HttpGet attribute will for sure take care of that.
For complex objects, I usually send them in the message body rather than the URL.
Would you have any objection to an approach similar to the answer of this question?
How to pass json POST data to Web API method as object
It seems like the more straightforward/natural approach.
Something like (untested, but should be close):
[RoutePrefix("api/titleestimate")]
public class TitleEstimateController : ApiController
{
[HttpGet]
public IHttpActionResult GetTitleEstimate([FromBody] EstimateQuery query)
{
// All the values in "query" are null or zero
// Do some stuff with query if there were anything to do
}
}
public class EstimateQuery
{
public string QueryName{ get; set; }
public string Whatever{ get; set; }
}
$(function () {
var query = {QueryName:"My Query",Whatever:"Blah"};
$.ajax({
type: "GET",
data :JSON.stringify(query),
url: "api/titleEstimate",
contentType: "application/json"
})
.done(function (data) {
$('#product').text("OK");
})
.fail(function (jqXHR, textStatus, err) {
$('#product').text('Error: ' + err);
});
});
I have a post request I am doing like so:
var addProject = function (project, id) {
return $.ajax(projectUrl, {
type: "POST",
data: { project: project, id: id }
});
};
This is all fine, and it send up my project object (in JSON) with no problem. What i want to do is this request but also add in a key (that does not belong in the project object) that I can pass to the server controller. Any idea if I can do this, or what is a good way to do this. I really don't want to have to go back and change my object models to account for a key that I will only need every once in awhile.
This is an ASP.NET MVC4 project, and it is going back to a web api controller if that matters.
EDIT: here is my server side stuff
public HttpResponseMessage PostNew(Project project, int id)
{
//TODO grab the client to add the project to
Uow.Projects.Add(project);
Uow.Commit();
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, project);
//response.Headers.Location = new Uri(Url.Link("ApiControllerAction", new { id = client.ID }));
return response;
}
Maybe I am not being clear enough. I want to pass in the project object, and then just an int variable. Passing project alone works, but if I try to pass the int it gives me a 400 error
var addProject = function (project) {
return
$.ajax(projectUrl, {
type: "POST",
data: {data1 : 'Object',data2: 'variable'}
});
};
You have just to send 2 data params., object and var..
Easiest way to pass a complex object is to JSON-encode it:
var addProject = function (project) {
return $.ajax(projectUrl, {
type: "POST",
contentType: 'application/json',
data: JSON.stringify(project)
});
};
To decode this on the server side, you will need a JSON decoder of some kind. I'm sure C# has some available.