Introduction
This is a guide how to
implement Web API2. Here is a list of requirements:
·
We shall
use the all the HTTP Verbs
·
When an
error or a warning occurs the details shall be a part of the response
·
The
implementation shall keep the server threads available for serving other
requests
·
The API
should be callable across multiple domains
·
Shall use
Windows Authentication
Installation of WebAPI2
Here are the steps
that I took in implementing WebApi from VS2012 update 4
1.
Create an MVC4 project
2.
Select the
WebApi template
This results in a WebApi with EF5 and some other old package
3.
Right
mouse click on the project and select Manage NuGet Packages
4.
Upgrade
all does work, but be aware that MVC5
uses and older JQuery library for compatibility reasons with some of the MVC
unobtrusive validation features depend on an older version of JQuery
5.
Add
references to MyApplication projects
Pre.MyApplication.Contracts
Pre.MyApplication.DAL
6.
Change the
web.config to add the MyApplication specific configuration sections
<connectionStrings>
<add name="MYAPPLICATION" connectionString="metadata=res://*/MyApplicationModel.csdl|res://*/MyApplicationModel.ssdl|res://*/MyApplicationModel.msl;provider=System.Data.SqlClient;provider
connection string="data source=servername;initial
catalog=MYAPPLICATION;integrated
security=True;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" />
</connectionStrings>
<appSettings>
<add key="webpages:Version" value="2.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="PreserveLoginUrl" value="true" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
<add key="CoreDataServiceRestURL" value="http://EWP-S-PRE/EWPDataServiceRest/" />
<add key="ApprovalWorkflowURL" value="http://localhost:18245/Approval.xamlx" />
<add key="MyServiceWorkflowURL" value="http://localhost:18245/MyService.xamlx" />
</appSettings>
When you start the
default website you will get the error The
object has not yet been initialized. Ensure that
HttpConfiguration.EnsureInitialized() is called in the application's startup
code after all other initialization code.
To fix this goto
Global.asax.cs and in the Application_Start:
//WebApiConfig.Register(GlobalConfiguration.Configuration);
GlobalConfiguration.Configure(WebApiConfig.Register);
The next message you
will get is Inheritance security rules
violated by type: 'System.Web.Mvc.MvcWebRazorHostFactory'. Derived types must
either match the security accessibility of the base type or be less accessible.
To fix this change the version from Version=4.0.0.0 to Version5.0.0.0
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral,
PublicKeyToken=31BF3856AD364E35" />
Automated Api documentation
This works via the
NuGet Package Microsoft.AspNet.WebApi.HelpPage. To get this to work do the
following steps:
1.
Goto the
build properties of the WebAPI project
2.
In the
Output section of the Build tab enable XML documentation file
3.
Change the
path from
bin\Pre.MyApplication.WebApi.XML to App_Data\Pre.MyApplication.WebApi.XML
4.
Goto
Area/HelpPage/App_Start/HelpPageConfig.cs
5.
Uncomment the following line
// Uncomment the following to use the documentation
from XML documentation file.
config.SetDocumentationProvider(new XmlDocumentationProvider(
HttpContext.Current.Server.MapPath("~/App_Data/Pre.MyApplication.WebApi.XML")));
To use the automated
documentation features you need to comment the WebApi controller. Here is an
example:
///
/// MyApplication Users
///
public class UserController : ApiController
{
///
/// Gets specified User
///
/// User
Id
/// The user
public UserModel Get(int id)
{
...
Implementing return code
Use IHttpActionResult
as in the following example
[ResponseType(typeof(UserModel))]
public IHttpActionResult Get(int id)
{
UserModel result = new UserModel()
{
ErrorLevel = "Warning",
ErrorMessage = "Not
Implemented yet!"
};
User u = new User() { Id = 1, ADUserName = "nfindlater",
DefaultRoutingGroupId = 1 };
result.Data = u;
var helper = new UrlHelper(Request);
result.Url = helper.Link("User", new { userId = 1 });
return Ok (result);
}
Note that you need to
specify [ResponseType(typeof(UserModel))]
because otherwise the sample code will be missing the API documentation
Routing and Versioning
There are a number of
way to version REST APIs. The following closely matches the existing WCF
versioning policy. To set this up you need to do the following:
1.
WebApiConfig.cs
file you need to add
config.MapHttpAttributeRoutes();
//This matches the url to use a
controller
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Here is an example of how to make
use of these attributes
namespace Pre.MyApplication.WebApi.Controllers
{
///
/// MyApplication Users
///
[RoutePrefix("2013/12/05")]
public class UserController : BaseApiController
{
///
/// Gets specified User
///
/// User
Id
/// The user
[Route("user/{id}")]
[ResponseType(typeof(UserModel))]
public IHttpActionResult Get(int id)
{
..
}
In the above example all methods in the UserController
are prefixed with 2013/12/05. This provides a versioning of this controller
that enables multiple versions of the User WebApi to be instanciable in the
same project.
The Route attributes also provide much greater
flexibility in the design of the REST API
Route
|
Description
|
[Route("patients/{id}/medications")]
|
Easier to get to "nested" resources
|
[Route["patients/{id:int:min(1)}"]
|
Constraints
|
[Route("patients/{id:int}")][Route["patients/{name:alpha}"]
|
Multiple routes peraction
|
[Route("patients/{id?}")]
|
Can apply to entire controller
|
[RoutePrefix("api/patients")]
|
Prefix for all other routes in controller
|
[RouteArea("Admin")]
|
Attributes for MVC Areas
|
CORS Cross Origin Resource Sharing
When a browser makes a
request to a server an “Origin : url” header is sent. If the origin is
different to that of the server and the server allows CORS the header returned
is “Access-Control-Allowed-Origin: OrginalUrl”. Then the browser will allow the
REST call to be made even though the JavaScript is hosted on a different domain
To get this to work
the following needs to be done:
1.
Right
Mouse click on the WebApi project and select Managed NuGet Packages
2.
Select
Microsoft ASP.NET WebApi 2 Cross-Origin Support
3.
In the
WebApiConfig.cs
using System.Web.Http.Cors;
..
public static void Register(HttpConfiguration config)
var cors = new EnableCorsAttribute("*", "*", "GET,POST");
config.EnableCors(cors);
An alternative
approach is to use the EnableCorsAttribute in the controller
[EnableCors("*", "*", "GET,POST")]
[RoutePrefix("2013/12/05")]
public class UserController : BaseApiController
{
Authentication
Set the Website to not
allow anonymous users but to use Windows Authentication. The server
authenticates the user according to some configuration in the machine.config
file
If the user is
not authenticated then the browser prompts the user for a user name and
password and authenticates the user.
To take
advantage of this you can decorate your controller with attributes. Here is an
example:
[AllowAnonymous]
public class HomeController : Controller
{
[Authorize(Roles="MyRole",Users="MyUser")];
public ActionResult Index()
{
return View();
}
}
[This needs some further work because we would want
to make a configuration file that would specify resource, action and ADGroup
JSON CamelCasing
In C# there is a
PascalCasing convention. In JavaScript there is a camelCasing convention. To
enable C# POCO classes that have Pascal Cased variables to be used in
JavaScript with camel Casing add the following to the WebApiConfig.cs
var jsonFormatter = Config.Formatters.OfType<JsonMediaTypeFormatter>().FirstOrDefault();
jsonFormatter.SerializerSettings.ContractResolver = new
CamelCasePropertyNamesContractResolver();
Adding error information in the REST response
This can be done by
using the following base class::
public abstract class BaseModel
{
public string Url { get; set; }
public T Data { get; set; }
public string ErrorLevel { get; set; }
public string ErrorMessage { get; set; }
}
This can be used in
the following way:
namespace Pre.MyApplication.WebApi.Models
{
public class UserModel : BaseModel<User>
{
}
}
[Route("user/{id}")]
[ResponseType(typeof(UserModel))]
public IHttpActionResult Get(int id)
{
UserModel result = new UserModel()
{
ErrorLevel = "Warning",
ErrorMessage = "Not
Implemented yet!"
};
User u = new User() { Id = 1, ADUserName = "nfindlater", DefaultRoutingGroupId = 1 };
result.Data = u;
var helper = new UrlHelper(Request);
result.Url =
helper.Request.RequestUri.AbsoluteUri;
return Ok (result);
}
dsd
UrlHelper is used to determine the url of the what ever
server is used to host this REST service. The resulting response looks like:
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles:
=?UTF-8?B?RDpcTmV3IGZvbGRlclxXb3JrZmxvdyAyLjBcU291cmNlXFRBT3BzXFByZS5UQU9wc1xQcmUuVEFPcHMuV2ViQXBpXDIwMTNcMTJcMDVcVXNlclwx?=
X-Powered-By: ASP.NET
Date: Wed, 27 Nov 2013 10:11:31 GMT
Content-Length: 191
”data":
{"id":1,
"adUserName":"nfindlater",
"name":null
,"defaultRoutingGroupId":1},
"errorLevel":"Warning"
,"errorMessage":"Not
Implemented yet!"}
The url is a useful
piece of information. In this case it identifies where the information comes
from. In a list this can be useful. The use of this style of url can be
extended in paginated lists. For performance reasons you never want to dump the
entire contents of a table. Instead you page through contents. This can be made
using code that looks like the following:
//var totalCount = result.Count();
//var totalPages = Math.Ceiling((double)totalCount /
PAGE_SIZE);
//var helper = new UrlHelper(Request);
//var prevUrl = page > 0 ?
helper.Link("User", new { page = page - 1 }) : "";
//var nextUrl = page < totalPages - 1 ?
helper.Link("User", new { page = page + 1 }) : "";
//var results = result.Skip(PAGE_SIZE * page)
// .Take(PAGE_SIZE)
// .ToList()
// .Select(f =>
TheModelFactory.Create(f));
Implementation of Asynchronous calls
There are a limited number of threads available for
processing REST calls. To keep these free the REST functions can be asynchronous,
here is an example of how this can be done:
///
/// Gets specified User
///
/// User
Id
/// The user
[Route("user/{id}")]
[ResponseType(typeof(UserModel))]
public async Task<IHttpActionResult> Get(int id)
{
//http://localhost:55528/2013/12/05/user
UserModel result = new UserModel()
{
ErrorLevel = "Warning",
ErrorMessage = "Not
Implemented yet!"
};
// Long running task goes here
Task<User> t = new Task<User>(() => {
User u = new User() { Id = 1,
ADUserName
= "nfindlater",
DefaultRoutingGroupId
= 1 };
return u;
});
t.Start();
await t;
result.Data = t.Result;
var helper = new UrlHelper(Request);
result.Url = helper.Request.RequestUri.AbsoluteUri;
return Ok(result);
}
Making Json the default return type
To make the default
Json we can add the following to WebApiConfig.cs
// Remove XML so that all responses are made in Json
// This is important for the Chrome browser
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes
.FirstOrDefault(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
WebAPI caching
When we have static data such as the UsersByTaskAndRoutingGroup we want to avoid a time consuming
database hit. This can be done by caching the data on the WebAPI side. To do
this do the following
1. Install the nugget package CacheCow
>Install-Package CacheCow.Server
2. Decorate methods with [HttpCacheControlPolicy(true, 120)]
Here’s an example
[HttpCacheControlPolicy(true, 120)]
[Route("UsersByTaskAndRoutingGroup/{name}")]
[ResponseType(typeof(UsersByTaskAndRoutingGroupListModel))]
public async Task<IHttpActionResult> Get(String name)
{
...
return Ok(result);
}
This reduced a REST
call that took 2s to a call the second time around to 300ms
Exception handling
We will handle
exceptions within the WebAPI project. In this way we centralize the error
handling functions at a higher level. We decided to use try catch blocks in the
web api methods and make the exceptions more readable using a method in the
base class:
namespace Pre.MyApplication.WebApi.Controllers
{
///
/// Base class for controllers needing access to the MyApplication
database
///
public abstract class BaseApiController : ApiController
{
...
///
/// This is used to help extract important information
form exceptions
///
/// The
exception
/// A string containing a summary of
usefull debugging information
public string MakeReadableExceptionMessage(Exception ex)
{
if (ex is DbEntityValidationException)
{
// These are errors ccoming from
EF
string outerErrors = "";
string outerDel = "";
DbEntityValidationException eV = ex as DbEntityValidationException;
foreach (var entity in eV.EntityValidationErrors)
{
string innerDel = "";
string ErrorMsg = "";
foreach (var e in entity.ValidationErrors)
{
ErrorMsg = e.PropertyName + " " + e.ErrorMessage;
outerErrors += innerDel + ErrorMsg;
innerDel = ",";
}
outerErrors += outerDel + ErrorMsg;
outerDel = ";";
}
return outerErrors;
}
else if (ex is DbUpdateException)
{
// These are errors coming from
the database
string Message =
ex.InnerException.InnerException.ToString();
return Message;
//SqlException s =
e.InnerException.InnerException as SqlException;
//if (s != null &&
s.Number == 2627)
//{
//}
}
else
{
return ex.Message;
}
}
This is used in the WebApi in the
following way:
[HttpGet]
[Route("MyService")]
[ResponseType(typeof(MyServiceListModel))]
public async Task<IHttpActionResult> Get()
{
//http://localhost:55528/2013/12/05/MyService
MyServiceListModel result = new MyServiceListModel();
var helper = new UrlHelper(Request);
result.Url = helper.Request.RequestUri.AbsoluteUri;
try
{
Task<List<MyService>> t = new Task<List<MyService>>(() =>
{
List<MyService> list = new List<MyService>();
//throw new
DivideByZeroException();
list = this.Db.MyServiceList();
return list;
});
t.Start();
await t;
Task.WaitAll(t);
result.DataList = t.Result;
}
catch (Exception ex)
{
result.ErrorMessage = MakeReadableExceptionMessage(ex);
result.ErrorLevel = "Error";
Request.CreateResponse(HttpStatusCode.BadRequest,
result);
}
return Ok(result);
}
This is work in
progress. We should log these errors meaning we need mockable loggers. In other
words a logger that implements an ILogger interface. We can also handle un
handled exceptions either adding the following to the Global.asax.cs file:
AppDomain.CurrentDomain.UnhandledException
+= (s, e) => {
// This one hardly get's called at all
// Your logging logic
};
AppDomain.CurrentDomain.FirstChanceException
+= (s, e) => {
// This one get called quite a few times
when simulating a divide by zero error
// Your logging logic
};
Referencing WF services
Here are the steps
needed to include asynchronous calls to the WF services
1.
Right
Mouse click on References
2.
Select Add
new Service Reference…
3.
Click the
Discover button
4.
Select the
appropriate xamlx file (for example MyWorkflow.xamlx
5.
Use an
appropriate namespace eg MyServiceService
6.
Click the
advanced button
7.
Check the
Allow generation of asynchronous operations
8.
Select
Generate Task based operations
If you get the error
Error 4 Custom
tool error: Failed to generate code for the service reference 'MyServiceService'.
Please check other error and warning messages for details. D:\New
folder\Workflow 2.0\Source\MyApplication\Pre.MyApplication\Pre.MyApplication.WebApi\Service
References\MyServiceService\Reference.svcmap 1 1 Pre.MyApplication.WebApi
Then you may
need to uncheck the Reuse types in all referenced assemblies from Configure
service reference option
Here’s a code
snippet how to use services asynchronously:
[HttpPost]
[Route("MyServiceNew")]
[ResponseType(typeof(Pre.MyApplication.WebApi.Models.MyServiceResponse))]
public async Task<IHttpActionResult> PostMyServiceNew([FromBody]JToken data)
{
try
{
MyServiceNewRequest value = DeserializeJson<MyServiceNewRequest>(data);
MyServiceService.MyServiceResponse wfResponse = new MyServiceService.MyServiceResponse();
using (MyServiceService.MyServiceWFClient wfClient = new MyServiceService.MyServiceWFClient())
{
string url = ConfigurationManager.AppSettings["MyServiceWorkflowURL"].ToString();
wfClient.Endpoint.Address = new EndpointAddress(url);
wfResponse = await wfClient.MyServiceNewAsync(value);
}
Deploayment
Here is how to setup
the WebAPI infrastructure
1.
First
Browse to the folder that is named after the DNS entry to the web site. For
example under D:\WebServices\MYAPPLICATION-S
2.
Create a
subdirectory such as MYAPPLICATION_20131205
The rational is that we want to make it possible to run 2 versions of the
WebApi at the same time. This would be needed for smooth migration from one
version to another. The naming is choose to represent the web service and the
version of the web service. The idea is that this can be shared without having
to rename the share from the folder name
3.
Right
mouse click the newly created folder select properties. Then select Sharing and
click the Advanced sharing button
4.
Check the
share folder box
5.
Click
permissions and give appropriate permissions
7.
Open the
Internet Information Services (IIS) Manager and browse to MYAPPLICATION-S
8.
Next right
mouse click on the newly created subdirectory and Add Application
9.
Change the
alias to just have YYYYMMDD The idea is that we don’t want to repeat too much
information in the url later used in the WebApi
Setting up Visual studio to deploy WebAPI
First you need to
setup a compiler configuration.
1.
Right
mouse click on the solution and select configuration manager
2.
Click the
Configuration Manager button
3.
Under the
Configuration column select combo box against the project that you want to add
a configuration to and Select
4.
Create a
new configuration with a name like TEST and copy settings from Release and
check the Create new solution configurations check box
5.
Right
mouse click on the Web.config
6.
Add Config
Transform
7.
Then you
get an extra web.config
Next setting up the
publish:
1.
Right
mouse click on the WebApi project and select Publish
2.
Click the
Select or import a publish profile and select “”
3.
Give the
profile a name like TEST
4.
Under
Publish method select File system and enter the path to where you want to
publish to
5.
Click next
6.
Change
Configuration to TEST
7.
Click
Publish