BackGround
There are numerous articles written about ASP.NET Web API technology. A lot can be found in CodeProject (
ASP.NET Web API), the
ASP.NET Web API official web site, as well as additional places. The following series of articles will try to explain the technology by applying on Data Streaming, HTTPS and extending Web API documentation. The articles assume you have basic knowledge of
C#, ASP.NET, OOP, ASP.NET MVC, REST service, IIS and some technical terms of .NET framework. With that being said, the articles will be presented with the following order.
- Introduction
- Data Streaming
- Working with HTTPS
- Extending Web API Documentation
Requirements and Dependencies
- The solution is best viewed by VS 2012(SP4) Ultimate and above. But it's also possible to view each individual project using VS Professional or Express 2012 and above.
- .NET framework 4.5.1 and above
- A lot of Nuget packages. See each project Nuget packages.config file
Things that will be covered in the articles
- Related to Asp.NET Web API Technology
- ActionFilter
- AuthorizationFilter
- DelegateHandler
- Different Web API routing attributes
- MediaTypeFormatter
- OWIN
- Self Hosting
- Web API documentation and its extension
- Related to .NET framework and other
- Async/Await
- .NET reflection
- Serialization
- ASP.NET Web API/MVC Error handling
- IIS ,HTTPS and Certificate
- Design principles and techniques
Introduction
It’s been years since ASP.NET Web API was added to the .NET framework. ASP.NET Web API is built on top of HTTP protocol that exposes RESTful services and data to external systems. The target is to reach services to various platforms using HTTP technology, which is supported by many end user applications, browsers, mobile devices, other services. ASP.NET Web API is a request-response message exchange pattern, in which a client can request certain information from a server and a server responses the request to the client. The response can be expected synchronously or asynchronously. When we think about a Web API usually several things pop up in your mind. Personally I'll point out these three basic key points regardless of the Web API implementation.
- The purpose of the service and its methods.
- Each method input(s) i.e. Request
- Each method output i.e. Response.
By convention, ASP.NET Web API lets you define method with a matching semantic to HTTP methods(verbs). For example, if we have a method name GetPysicians()
, then its matching name to the HTTP method will be GET
. In short, the following diagram below shows the matching of each method to the HTTP methods (verbs).
But this approach may not be convenient for different scenarios. For example, you may want to define multiple GET
or POST
methods within a single API controller class. In this case, the framework lets you define an action level route to include the action as part of a request URI. The following code shows how this can be configured.
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(name: "PhysicianApi",
routeTemplate: "{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional });
}
Still this may not be sufficient to satisfy other scenarios. Suppose you want a method that deletes a file from a centralized repository and you want to use the same method to get the file as well. In a situation like this, the the Web API framework lets you to decorate the action with Get
and Delete
HTTP method attributes. See the picture below.
Notice that RemoveFile
method can be invoked by using Delete(HttpDelete
) or Get(HttpGet
) verbs. HTTP method attribue is also helpful to define a suitable name for a service method.
The other feature the framework provided is that the ability to facilitate an attribute routing template. This is similar to ASP.NET MVC routing except it relay on HTTP Methods (verbs) not the URI of the action. This lets you define several types of actions under the Web API service. For example, you can define a parameterized URI's service methods. So when a request is made to a service method, you can pass a parameter as part of the request URI. The following example code shows a Web API attribute routing on GetPhysicianBySpeciality
method.
[Route("physician/{speciality}/{physicianID}")]
public PhysicianBase GetPhysicianBySpeciality(string speciality, int physicianID)
{
}
So this will facilitate the client to send a request like http://localhost/physician/eyecare/1. There are various types of routing attributes that helps to decorate the Web API controller and its methods. These are:
ActionName | lets you define action name routing |
Http Methods(HttpGet, HttpPost, AcceptVerbs ...) | lets you define HTTP methods(verbs) |
NonAction | prevent the action not being invoked |
Route | lets you define template along with/without parameter |
RoutePrefix | lets you define a controller prefix name |
ASP.NET Web API is also enriched with useful libraries and classes like HttpClient
, HttpRequestMessage
, and HttpResponseMessage
. See the reference section for list of Web API references. This is by far enough as introduction. In the following section, I'll explain one of the main areas of the article.
Data Streaming
One of frequently performed operation through the internet is data streaming. ASP.NET Web API is capable of processing large size of stream data from/to the server/client. The stream data can be a file located on a directory or binary data stored on database. In this article, there are two basic methods, namely Download
and Upload
are involved to accomplish data streaming. The download is responsible for pulling stream data from the server whereas upload is responsible for saving stream data to server.
Note: This article explains data streaming on resources that are located in specific(configurable) directories and be able to streamed through Web API service.
Participating projects
WebAPIDataStreaming
WebAPIClient
POCOLibrary
Before explaining the code section, there are certain configurations that need to be done on IIS (7.5) and Web API service web.config file.
- Make sure Downloads/Uploads directories/files are granted the necessary permissions (Read/Write) to a user (IIS_IUSRS)
- Make sure there is enough memory (RAM) and Hard Disk space available to process large sized files.
- For larger sized file data,
- Make sure
maxRequestLength
along with a reasonable executionTimeout
are configured in web.config file. The value can vary depending on the size allowed to be streamed. The maximum allowed file size is 2GB.
- Make sure
maxAllowedContentLength
is configured under requestFiltering
configuration section of web.config file. The default value for this setting is approximately 30MB and the max value is 4GB.
Note: In order to use maxAllowedContentLength
configuration, Request filtering feature should be enabled on IIS. Here is how to do so.
- Go to Control Panel
- Select Turn Windows Features on or off on the left side of the Programs and Features menu
- Select Internet Information Services > World Wide Web Services > Security
- Enable Request Filtering
Details of request filtering can be found in reference section
Once these pre-configuration are completed, it is easy to create and consume data stream Web API service. The first thing to do is to define the file streaming
ApiController
along with the necessary operations. As I stated earlier, the main methods of the file streaming service are download and upload. But, it also contains other methods related to file streaming.
[RoutePrefix("filestreaming")]
[RequestModelValidator]
public class StreamFilesController : ApiController
{
[Route("getfilemetadata")]
public HttpResponseMessage GetFileMetaData(string fileName)
{
}
[HttpGet]
[Route("searchfileindownloaddirectory")]
public HttpResponseMessage SearchFileInDownloadDirectory(string fileName)
{
}
[Route("downloadasync")]
[HttpGet]
public async Task<HttpResponseMessage> DownloadFileAsync(string fileName)
{
}
[Route("download")]
[HttpGet]
public HttpResponseMessage DownloadFile(string fileName)
{
}
[Route("upload")]
[HttpPost]
public HttpResponseMessage UploadFile(bool overWrite)
{
}
[Route("uploadasync")]
[HttpPost]
public async Task<HttpResponseMessage> UploadFileAsync(bool overWrite)
{
}
}
The Download
service method works first by checks the requested file name existence in the desired file path. If file is not found,it returns an error response object saying "file is not found". If it succeeds, it reads the content as bytes and attaches to the response object as an application/octet-stream
MIMI content type.
[Route("download")]
[HttpGet]
public HttpResponseMessage DownloadFile(string fileName)
{
HttpResponseMessage response = Request.CreateResponse();
FileMetaData metaData = new FileMetaData();
try
{
string filePath = Path.Combine(this.GetDownloadPath(), @"\", fileName);
FileInfo fileInfo = new FileInfo(filePath);
if (!fileInfo.Exists)
{
metaData.FileResponseMessage.IsExists = false;
metaData.FileResponseMessage.Content = string.Format("{0} file is not found !", fileName);
response = Request.CreateResponse(HttpStatusCode.NotFound, metaData, new MediaTypeHeaderValue("text/json"));
}
else
{
response.Headers.AcceptRanges.Add("bytes");
response.StatusCode = HttpStatusCode.OK;
response.Content = new StreamContent(fileInfo.ReadStream());
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
response.Content.Headers.ContentDisposition.FileName = fileName;
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
response.Content.Headers.ContentLength = fileInfo.Length;
}
}
catch (Exception exception)
{
metaData = new FileMetaData();
metaData.FileResponseMessage.Content = ProcessException(exception);
response = Request.CreateResponse(HttpStatusCode.InternalServerError, metaData, new MediaTypeHeaderValue("text/json"));
}
return response;
}
The Upload
service method works on top of a multipart/form-data
MIMI content type. First it checks the HTTP request content type is a type of multipart. If it succeeds, it compares the content length to the maximum allowed file size to be uploaded. If it succeeds, it starts to upload the request content to the desired location. When the operation is completed, it notifies the user with appropriate response message. The code fragment that performs upload is shown below.
[Route("upload")]
[HttpPost]
public HttpResponseMessage UploadFile(bool overWrite)
{
HttpResponseMessage response = Request.CreateResponse();
List<FileResponseMessage> fileResponseMessages = new List<FileResponseMessage>();
FileResponseMessage fileResponseMessage = new FileResponseMessage { IsExists = false };
try
{
if (!Request.Content.IsMimeMultipartContent())
{
fileResponseMessage.Content = "Upload data request is not valid !";
fileResponseMessages.Add(fileResponseMessage);
response = Request.CreateResponse(HttpStatusCode.UnsupportedMediaType, fileResponseMessages, new MediaTypeHeaderValue("text/json"));
}
else
{
response = ProcessUploadRequest(overWrite);
}
}
catch (Exception exception)
{
fileResponseMessage = new FileResponseMessage { IsExists = false };
fileResponseMessage.Content = ProcessException(exception);
fileResponseMessages.Add(fileResponseMessage);
response = Request.CreateResponse(HttpStatusCode.InternalServerError, fileResponseMessages, new MediaTypeHeaderValue("text/json"));
}
return response;
}
[Route("uploadasync")]
[HttpPost]
public async Task<HttpResponseMessage> UploadFileAsync(bool overWrite)
{
return await new TaskFactory().StartNew(
() =>
{
return UploadFile(overWrite);
});
}
private HttpResponseMessage ProcessUploadRequest(bool overWrite)
{
}
The client app that calls the download and upload file method is a console app. The app consumes the file streaming Web API service through HttpClient
and related classes. Basically the download file code creates a download HTTP request object with a proper file name and sends the request to the server.
private static async Task DownloadFile()
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Please specify file name with extension and Press Enter :- ");
string fileName = Console.ReadLine();
string localDownloadPath = string.Concat(@"c:\", fileName);
bool overWrite = true;
string actionURL = string.Concat("downloadasync?fileName=", fileName);
try
{
Console.WriteLine(string.Format("Start downloading @ {0}, {1} time ",
DateTime.Now.ToLongDateString(),
DateTime.Now.ToLongTimeString()));
using (HttpClient httpClient = new HttpClient())
{
httpClient.BaseAddress = baseStreamingURL;
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, actionURL);
await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).
ContinueWith((response)
=>
{
Console.WriteLine();
try
{
ProcessDownloadResponse(localDownloadPath, overWrite, response);
}
catch (AggregateException aggregateException)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(string.Format("Exception : ", aggregateException));
}
});
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
}
}
private static void ProcessDownloadResponse(string localDownloadFilePath, bool overWrite,
Task<HttpResponseMessage> response)
{
if (response.Result.IsSuccessStatusCode)
{
response.Result.Content.DownloadFile(localDownloadFilePath, overWrite).
ContinueWith((downloadmessage)
=>
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(downloadmessage.TryResult());
});
}
else
{
ProcessFailResponse(response);
}
}
Notice the code above. The HttpClient
object sends the request and waits the response to send only the response header (HttpCompletionOption.ResponseHeadersRead
), not the entire response content of the file. Once the response header is read, it performs the necessary validation on the content and if it succeeds, the actual file downloading method will be executed.
And here is the code that calls the upload file streaming Web API service method. Similar to download method, it creates a request object with a multipart form data content type and sends the request to the server. The content is validated and sent to server to be processed further.
private static async Task UploadFile()
{
try
{
string uploadRequestURI = "uploadasync?overWrite=true";
MultipartFormDataContent formDataContent = new MultipartFormDataContent();
formDataContent.AddUploadFile(@"c:\nophoto.png");
formDataContent.AddUploadFile(@"c:\ReadMe.txt");
if (!formDataContent.HasContent())
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Write(formDataContent.GetUploadFileErrorMesage());
return;
}
else
{
string uploadErrorMessage = formDataContent.GetUploadFileErrorMesage();
if (!string.IsNullOrWhiteSpace(uploadErrorMessage))
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Write(uploadErrorMessage);
}
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uploadRequestURI);
request.Content = formDataContent;
using (HttpClient httpClient = new HttpClient())
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(string.Format("Start uploading @ {0}, {1} time ",
DateTime.Now.ToLongDateString(),
DateTime.Now.ToLongTimeString()));
httpClient.BaseAddress = baseStreamingURL;
await httpClient.SendAsync(request).
ContinueWith((response)
=>
{
try
{
ProcessUploadResponse(response);
}
catch (AggregateException aggregateException)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(string.Format("Exception : ", aggregateException));
}
});
}
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
}
}
private static void ProcessUploadResponse(Task<HttpResponseMessage> response)
{
if (response.Result.IsSuccessStatusCode)
{
string uploadMessage = string.Format("\nUpload completed @ {0}, {1} time ",
DateTime.Now.ToLongDateString(),
DateTime.Now.ToLongTimeString());
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(string.Format("{0}\nUpload Message : \n{1}", uploadMessage,
JsonConvert.SerializeObject(response.Result.Content.ReadAsAsync<List<FileResponseMessage>>().TryResult(), Formatting.Indented)));
}
else
{
ProcessFailResponse(response);
}
}
The data streaming project is also consists of extension classes and methods which are not explicitly explained in the article. Download the source code and explore it.
What's Next
The next section explains about Working with HTTPS
References
History and GitHub Version