Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generating a JS-client based on a ASP.NET WebAPI Controller

Tags:

In modern web-projects that use RESTful API's we often see AJAX-calls like the one below littered around our JavaScript-files.

$.ajax({     type: "POST",     url: myapp.baseUrl + 'Api/Note',     data: ko.mapping.toJSON(note),     contentType: 'application/json', }).done(function (response) {     // do something }).fail(function (jqxhr) {     // do something else }); 

I love WebAPI, I love Knockout and I love tying the two together. However these AJAX-calls are quite verbose and contain all kinds of details that I am not really interested in. So instead, I create a wrapper around these methods:

myapp.api.saveNote(note) 

However this still requires me to actually write a wrapper containing the AJAX-call. I was wondering if you could actually generate these wrappers. In essence, I would be generating a JS-based client for my WebAPI, similar to how Java and .NET can generate clients based on WSDL's.

  1. Has this been done before?
  2. Are there other ways to tie ASP.NET WebAPI and JavaScript together without writing a bunch of AJAX boilerplate code?
  3. In other words, are there frameworks for creating JS-interfaces based on server-side interfaces like ASP.NET WebAPI?

I've already looked at amplifyJS but this only partially solves the problem. I am looking for a solution that actually creates an interface based on the WebAPI-controllers in my solution. If this does not exist, I will start tinkering myself. I've already got an idea for a WebAPIClientGenerator that uses reflection to iterate over all ApiController's.

like image 622
Martin Devillers Avatar asked Aug 13 '13 19:08

Martin Devillers


People also ask

Can JavaScript call Web API?

The most Popular way to call a REST API from JavaScript is with the XMLHttpRequest (XHR) object. You can perform data communication from a URL of the Web API without having to do a full page refresh. Other methods for calling APIS in JavaScript are Fetch API and Promise.

Can we call Web API from MVC controller?

In order to add a Web API Controller you will need to Right Click the Controllers folder in the Solution Explorer and click on Add and then Controller. Now from the Add Scaffold window, choose the Web API 2 Controller – Empty option as shown below. Then give it a suitable name and click OK.

Can you use ASP net with JavaScript?

ASP.NET requires the use of the client-side Javascript, as that's how ASP.NET handles its Events via PostBacks.


1 Answers

Just found a project called: ProxyApi

ProxyApi is a library that automatically creates JavaScript proxy objects for your ASP.NET MVC and WebApi Controllers.

GitHub: https://github.com/stevegreatrex/ProxyApi

Blog: http://blog.greatrexpectations.com/2012/11/06/proxyapi-automatic-javascript-proxies-for-webapi-and-mvc/

ProxyApi generated invalid JavaScript for my solution which contained over a hundred separate WebAPI actions. This is probably because ProxyApi does not cover all WebApi features such as custom ActionName attributes. Moreover the ProxyApi library is a bit on the bulky side to my taste. There has to be a more efficient way to do this...

So I decided to take a look at the ASP.NET WebAPI source code and it turns out WebAPI has self-describing functionality built into it. You can use the following code from anywhere in your ASP.NET solution to access WebAPI metadata:

var apiExplorer = GlobalConfiguration.Configuration.Services.GetApiExplorer(); 

Based on the output from apiExplorer.ApiDescriptions, I rolled my own metadata provider:

public class MetadataController : Controller {     public virtual PartialViewResult WebApiDescription()     {         var apiExplorer = GlobalConfiguration.Configuration.Services.GetApiExplorer();         var apiMethods = apiExplorer.ApiDescriptions.Select(ad => new ApiMethodModel(ad)).ToList();         return PartialView(apiMethods);     }      public class ApiMethodModel     {         public string Method { get; set; }         public string Url { get; set; }         public string ControllerName { get; set; }         public string ActionName { get; set; }         public IEnumerable<ApiParameterModel> Parameters { get; set; }          public ApiMethodModel(ApiDescription apiDescription)         {             Method = apiDescription.HttpMethod.Method;             Url = apiDescription.RelativePath;             ControllerName = apiDescription.ActionDescriptor.ControllerDescriptor.ControllerName;             ActionName = apiDescription.ActionDescriptor.ActionName;             Parameters = apiDescription.ParameterDescriptions.Select(pd => new ApiParameterModel(pd));         }     }      public class ApiParameterModel     {         public string Name { get; set; }         public bool IsUriParameter { get; set; }          public ApiParameterModel(ApiParameterDescription apiParameterDescription)         {             Name = apiParameterDescription.Name;             IsUriParameter = apiParameterDescription.Source == ApiParameterSource.FromUri;         }     } } 

Use this controller in conjunction with the following view:

@model IEnumerable<Awesome.Controllers.MetadataController.ApiMethodModel> <script type="text/javascript">     var awesome = awesome || {};      awesome.api = {         metadata: @Html.Raw(Json.Encode(Model))     };      $.each(awesome.api.metadata, function (i, action) {         if (!awesome.api[action.ControllerName]) {             awesome.api[action.ControllerName] = {};         }         awesome.api[action.ControllerName][action.ActionName] = function (parameters) {             var url = '/' + action.Url;             var data;             $.each(action.Parameters, function (j, parameter) {                 if (parameters[parameter.Name] === undefined) {                     console.log('Missing parameter: ' + parameter.Name + ' for API: ' + action.ControllerName + '/' + action.ActionName);                 } else if (parameter.IsUriParameter) {                     url = url.replace("{" + parameter.Name + "}", parameters[parameter.Name]);                 } else if (data === undefined) {                     data = parameters[parameter.Name];                 } else {                     console.log('Detected multiple body-parameters for API: ' + action.ControllerName + '/' + action.ActionName);                 }             });             return $.ajax({                 type: action.Method,                 url: url,                 data: data,                 contentType: 'application/json'             });         };     }); </script> 

The controller will use the ApiExplorer to generate metadata about all available WebAPI actions. The view will render this data as JSON and then execute some JavaScript to transform this data to actual executable JavaScript functions.

To use this little bit of magic, insert the following line in the head of your Layout page after your jQuery reference.

@Html.Action(MVC.Metadata.WebApiDescription()) 

From now on, you can make your WebAPI calls look like this:

// GET: /Api/Notes?id={id} awesome.api.Notes.Get({ id: id }).done(function () {     // .. do something cool        });  // POST: /Api/Notes awesome.api.Notes.Post({ form: formData }).done(function () {     // .. do something cool        }); 

This simple proxy will automatically distinguish query string parameters from request body parameters. Missing parameters or multiple body-parameters will generate an error to prevent typo's or other common WebAPI development errors.

like image 75
Martin Devillers Avatar answered Oct 26 '22 00:10

Martin Devillers