Click here to Skip to main content
13,794,242 members
Click here to Skip to main content
Add your own
alternative version

Stats

53.7K views
1.5K downloads
101 bookmarked
Posted 5 Jan 2016
Licenced CPOL

Web API – A Solid Approach

, 7 Jan 2018
Rate this:
Please Sign up or sign in to vote.
This article demonstrates how to, very easily, create a lean Web API on top of a .NET library of stateless services. Although the sample code is quite simple, the described architecture is rock-solid.

Introduction

In an earlier CodeProject article I claimed - as an answer to one of the article comments - that the domain services library described in the article formed the perfect basis for a Web API. In this article, I will demonstrate this.

There might be multiple understandings of what a Web API actually is. In my understanding, it is a HTTP-based Web service. Meaning a Web service that truly embraces HTTP – the main application protocol of the Internet and the driver of the World Wide Web. A Web service that does not only use HTTP as the transportation protocol, but also actively uses  HTTP’s uniform interface (the HTTP methods GET, PUT, POST, DELETE etc.). It is a Web service without any SOAP or RPC abstraction layers on top of HTTP - only pure HTTP. You can consider it a kind of “back-to-basics” Web service.

Some also use the term a REST API. However, rather than being a synonym for a Web API, REST is an architectural style that you may or may not adhere to in a Web API. Actually, it can be quite hard to decide whether a Web API is RESTful or not. Martin Fowler has a nice description of the Richardson Maturity Model that can be helpful to decide whether a Web API is truly RESTful or not.

Background

In the previous article I introduced the following three assemblies (dlls):

Image of 3-layered structure

The DomainServices project contains the basic abstractions in the form of generic interfaces and abstract classes – for example a generic IRepository interface and an abstract BaseService class.

The MyServices project contains some concrete extensions of the DomainServices abstractions – for example, a ProductService and an IProductRepository interface.

The MyServices.Data project contains concrete implementations of the repository interfaces defined in MyServices – for example a ProductRepository, which is an implementation of IProductRepository that stores products, serialized in a JSON file.

In the sample code of this article, an extra assembly called MyServices.Web is added. This is the Web API. It contains a number of so-called controllers – for example a ProductController.

4-layered architecture

The main reason why it is very easy to build a Web API on top of the domain services is that these services are stateless. They do not maintain any information about the state of the consumers of the services (the clients). They do not track any information from call to call. This fits perfectly to a Web API because HTTP is a stateless protocol. This way the Web API can be implemented as a very lean layer – essentially as a kind of façade exposing the pure .NET services over HTTP.

Overall, the characteristics of this architecture are the following:

  • The repository pattern ensures the utmost flexibility and scalability when it comes to data provider technologies (databases, ORM systems, file formats etc.)

  • The true business functionality is confined in MyServices where it can be easily unit tested using fake repositories.

  • The business functionality in MyServices can be made available as a library for any type of .NET project – for example a Windows desktop application.

As the Web API layer is very lean, the choice of development framework is not critical, as it can be relatively easily exchanged.

Depicted as a dependency graph, it looks like this:

Dependency Graph

In this architecture, the Web API (and other UI’s) as well as data providers are merely implementation details. They are plug-ins to the core business functionality in the MyServices assembly. This is a very robust architecture because it allows major decisions to be deferred. That is, major decisions about for example UI, data providers and frameworks. A good architecture allows you to make decisions late. Later is always better when you make decisions, because then you have more information.

Now, let us dig into the details of the MyServices.Web sample code.

Programming Framework

The sample code is made using ASP.NET 4.5 Web API 2.2. This is not the latest Web API programming framework from Microsoft. The newer ASP.NET 5 introduces MVC 6, which combines the features of MVC and Web API into a single Web programming framework. However, since I claim above that the Web API can be relatively easily replaced with another Web API using another programming framework, it would be really nice to see someone grab this challenge and provide an alternative solution using for example MVC 6. Who is first?

The Controllers

The controller classes handle the incoming HTTP requests. It makes a lot of sense to create one controller class for each of the services in MyServices.  Here is the entire product controller class, with full CRUD-functionality:

[RoutePrefix("api/product")]
[ControllerExceptionFilter]
public class ProductController : ApiController
{
    private readonly ProductService _productService;

    public ProductController(ProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    [Route("all")]
    public IHttpActionResult GetAll()
    {
        return Ok(_productService.GetAll());
    }

    [Route("{id}")]
    public IHttpActionResult Get(Guid id)
    {
        return Ok(_productService.Get(id));
    }

    [HttpPost]
    [ValidateModel]
    public IHttpActionResult Add([FromBody] ProductDTO productDTO)
    {
        var product = productDTO.ToProduct();
        _productService.Add(product);
        return Created(string.Format("{0}/{1}", Request.RequestUri, product.Id), product);
    }

    [HttpPut]
    [ValidateModel]
    public IHttpActionResult Update([FromBody] UpdatedProductDTO productDTO)
    {
        var product = productDTO.ToProduct();
        _productService.Update(product);
        return Ok(_productService.Get(product.Id));
    }

    [Route("{id}")]
    public void Delete(Guid id)
    {
        _productService.Remove(id);
    }
}

As mentioned above, preferably the web services assembly shouldn’t contain much more than a number of lean controller classes. In my opinion, it doesn't get much leaner than the above product controller: a number of very short public methods (actions) that essentially redirect the job to the underlying services.

If a controller action returns an IHttpActionResult, as most of the actions do, the ASP.NET Web API framework provides some underlying functionality to asynchronously compose the HTTP response message. Web API comes with a number of built-in implementations of IHttpActionResult. For example Ok which will create a response with HTTP status code 200 (OK) or Created which will create a response with status code 201 (Created).

If the return type of an action is void, Web API simply returns an empty HTTP response with status code 204 (No Content). This is utilized in the Delete action, where this is exactly what is needed.

The ProductService instance is injected into the controller using constructor injection - a dependency injection (DI) pattern:

public class ProductController : ApiController
{
    private readonly ProductService _productService;

    public ProductController(ProductService productService)
    {
        _productService = productService;
    }

    ...
}

The DI container used in this project is Unity. On a side note, Unity was recently made open source. The registration of components is done in the UnityConfig class, which is placed in the App_Start folder:

public static class UnityConfig
{
    public static void RegisterComponents()
    {
        var container = new UnityContainer();
        var dataPath = Path.Combine(HttpRuntime.AppDomainAppPath, "App_Data");
        var productRepositoryPath = Path.Combine(dataPath, "products.json");
        var userRepositoryPath = Path.Combine(dataPath, "users.json");
        container.RegisterType<IProductRepository, ProductRepository>(new InjectionConstructor(productRepositoryPath));
        container.RegisterType<IUserRepository, UserRepository>(new InjectionConstructor(userRepositoryPath));
        GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
    }
}

An instance of a Unity container is created, and a product repository and a user repository are registered in this container. Both repositories are JSON file repositories located in the App_Data folder .

Routing

When the Web API receives a HTTP request, it attempts to route the request to an action in a controller. There are two ways to route the incoming requests to the proper action: convention-based routing or attribute routing.

To enable convention-based routing, define a generic route template using the MapHttpRoute method. This is done in the WebApiConfig class, which is placed in the App_Start folder.

config.Routes.MapHttpRoute("DefaultApi",
    "api/{controller}/{id}",
     new { id = RouteParameter.Optional });

Now, Web API looks at the incoming HTTP method, and then looks for an action whose name begins with that HTTP method name. For example, with a GET request, Web API looks for an action that starts with "Get", such as GetProduct or GetAll.  This convention applies to the GET, POST, PUT and DELETE methods.

To enable attribute routing, call MapHttpAttributeRoutes during configuration:

config.MapHttpAttributeRoutes();

You can now use the Route attribute to explicitly define routing for an action, for example retrieving a product with a specific ID:

[Route("api/product/{id}")]
public IHttpActionResult Get(Guid id)
{
    return Ok(_productService.Get(id));
}

The string "api/product/{id}" is the URI template for the route. In this URI template "{id}" is a placeholder for a variable parameter. The Route attribute can be used in combination with the RoutePrefix attribute, which defines a common routing prefix for all of the actions in a controller.

[RoutePrefix("api/product")]
public class ProductController : ApiController
{
    ...
}

The two routing mechanisms, convention-based routing and attribute routing, can be used in combination. Preferably, you should give precedence to attribute routing and use convention-based routing only as a fall-back mechanism. This is done by calling MapHttpAttributeRoutes before you start adding route conventions.

Passing parameters

The process of mapping the variable parameters of an HTTP request to the action parameters is called parameter binding. Simple parameters represented by primitive .NET types like string, int or Guid, can be passed directly through the URI, or alternatively by using query string parameters. This is the case in for example the Delete action of the product controller:

[Route("{id}")]
public void Delete(Guid id)
{
     _productService.Remove(id);
}

If you want to pass a more advance parameter, for example a product when adding a new product, then you will have to read it from the body of the HTTP request, by decorating the action parameter with a FromBody attribute. This is done in the Add action of the product controller:

[HttpPost]
[ValidateModel]
public IHttpActionResult Add([FromBody] ProductDTO productDTO)
{
    var product = productDTO.ToProduct();
    _productService.Add(product);
    return Created(string.Format("{0}/{1}", Request.RequestUri, product.Id), product);
}

Notice, that the domain model object (the Product class) is not exposed directly at the API boundary. I.e. there is no assumption that input data sent in the HTTP request can be directly de-serialized into a Product class. Instead, a ProductDTO data structure is introduced. This is a very robust approach because at the boundaries, applications are not object oriented. The role of the ProductDTO structure is to interpret and validate the received data and possibly convert it into a proper Product object. More about this below.

Model validation

When you create a new Web API project from scratch in Visual Studio, a Models folder is normally created by default. However, in the sample code of this article, the Models folder is removed because the domain model objects are already defined in the MyServices assembly. For example the Product class from the MyServices assembly is the domain model of a product. This is actually the fact that facilitates making the Web API itself very lean.

In ASP.NET Web API, you can use the DataAnnotations attributes (for example Required, Key and Range) to set validation rules for properties on the domain model. But as mentioned above, rather than handling validation at the model level itself, it is a more robust approach to handle this in the “interpretation layer” – the ProductDTO structure:

public struct ProductDTO
{
    public Guid Id { get; set; }

    [Required, Key]
    public string Name { get; set; }

    [Required, Range(1, double.MaxValue)]
    public decimal Price { get; set; }

    public Product ToProduct()
    {
        return new Product(Guid.NewGuid(), Name) { Price = Price };
    }
}

As seen from this data structure, it is not expected that a HTTP request to add a new product provides a product ID, as the Id property is not decorated with the Required attribute. Instead, the ID will be automatically generated when calling the ToProduct method.

The model validation process is a cross-cutting concern that is best solved by using aspect oriented programming practices. ASP.NET Web API supports this through so-called action filters. This way, you can create an action filter to check the model state before the controller action is invoked. This is most easily done by extending the ActionFilter attribute:

[AttributeUsage(AttributeTargets.Method)]
public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (actionContext.ModelState.IsValid == false)
        {
            actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest,
                actionContext.ModelState);
        }
    }
}

The ModelState dictionary is obtained from the HttpActionContext. An error response with status code 400 (Bad Request) is returned if the model state is invalid.

Now, this ValidateModel attribute can be applied on every action where you want to perform model validation. For example when adding a new user:

[HttpPost]
[ValidateModel]
public IHttpActionResult Add([FromBody] UserDTO userDTO)
{
    var user = userDTO.ToUser();
    _userService.Add(user);
    return Created(string.Format("{0}/{1}", Request.RequestUri, user.Id), user);
}

Exception Handling

Just like model validation, exception handling is a cross-cutting concern that can be handled using the interception capabilities of filters. By extending the ExceptionFilter attribute you can, for example, establish a mapping of thrown exceptions to HTTP status codes:

public class ControllerExceptionFilterAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext actionExecutedContext)
    {
        var e = actionExecutedContext.Exception;
        var response = new HttpResponseMessage();
        if (e is KeyNotFoundException || e is ArgumentOutOfRangeException)
        {
            response.StatusCode = HttpStatusCode.NotFound;
        }
        else if (e is ArgumentException)
        {
            response.StatusCode = HttpStatusCode.BadRequest;
        }
        else
        {
            response.StatusCode = HttpStatusCode.InternalServerError;
        }
        response.Content = new StringContent(e.Message);
        actionExecutedContext.Response = response;
    }
}

This exception filter will catch all exceptions that happen within an action or another action filter. It maps the KeyNotFoundException and ArgumentOutOfRangeException to HTTP status code 404 (Not Found) and ArgumentException to status code 400 (Bad Request). All other exceptions will be mapped to status code 500 (Internal Server Error).

Now, decorating the controllers with the ControllerExceptionFilter attribute will enforce this policy, and ensure that proper HTTP status codes are returned when exceptions are thrown:

[ControllerExceptionFilter]
public class ProductController : ApiController
{
    ...
}

Debugging and Hosting

To debug the sample code, set MyServices.Web as the start project and press F5. This will fire up under IIS Express. All GET requests can be tested directly in the browser. For example, to retrieve a list of all products, enter the following in the address bar of the browser:

http://localhost:53735/api/product/all

Testing POST, PUT and DELETE requests require a tool like for example Fiddler. To add a new product, compose a HTTP request like the following:

POST http://localhost:53735/api/product

Content-Type: application/json

{
  "Name": "Sprite",
  "Price": 1.95
}

When it comes to deployment, ASP.NET Web API can obviously be hosted on IIS. But other options are self-hosting using OWIN or Microsoft Azure.

Summary

You can create a very lean Web API by creating it as a façade to a collection of stateless .NET services, who own and expose the core business functionality. By treating the Web API as a plug-in to the underlying .NET services, you establish a very robust architecture that allows major decisions to be deferred - for example, the choice of Web API programming framework which is no longer that important, since it relatively easily can be replaced with another technology.

This solution is also very DI-friendly as the services can be injected into the controller classes using constructor injection.

Although the described architecture is rock-solid, for clarity, many production code concerns are left out of this article and the sample code. These concerns include for example: security, versioning, caching, content negotiation, cross-origin resource sharing (CORS), route constraints etc. However, there are tons of valuable resources out there providing help on these subjects.

The sample code is made in Visual Studio 2013 using ASP.NET 4.5 Web API 2.2.

In the sample code, all the NuGet package dependencies (Unity, xUnit.NET, Json.NET and ASP.NET Web API itself) are included, so you do not need to download these packages yourself to compile and run the sample code.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

L. Michael
Architect
Denmark Denmark
I am a software architect/developer/programmer.

I have a rather pragmatic approach towards programming, but I have realized that it takes a lot of discipline to be agile. I try to practice good craftsmanship and making it work.

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
Chairman9-Jan-18 10:01
memberChairman9-Jan-18 10:01 
GeneralRe: My vote of 5 Pin
L. Michael16-Jan-18 6:12
memberL. Michael16-Jan-18 6:12 
QuestionThis is a spectacular article about SOLID implementation inside web api in the internet Pin
Arul Manivannan6-Jul-17 23:01
memberArul Manivannan6-Jul-17 23:01 
QuestionUsing MongoRepository for Project Pin
Tung Hoang22-Jun-16 18:39
memberTung Hoang22-Jun-16 18:39 
AnswerRe: Using MongoRepository for Project Pin
L. Michael23-Jun-16 0:17
memberL. Michael23-Jun-16 0:17 
GeneralRe: Using MongoRepository for Project Pin
Tung Hoang23-Jun-16 18:50
memberTung Hoang23-Jun-16 18:50 
GeneralMy vote of 4 Pin
Muhammad Shahid Farooq9-Jun-16 2:54
professionalMuhammad Shahid Farooq9-Jun-16 2:54 
QuestionGreat Article : EF Integration Question Pin
hendog9826-Feb-16 8:41
memberhendog9826-Feb-16 8:41 
AnswerRe: Great Article : EF Integration Question Pin
L. Michael28-Feb-16 23:48
memberL. Michael28-Feb-16 23:48 
GeneralRe: Great Article : EF Integration Question Pin
Tung Hoang22-Aug-16 22:00
memberTung Hoang22-Aug-16 22:00 
GeneralRe: Great Article : EF Integration Question Pin
L. Michael2-Sep-16 5:12
memberL. Michael2-Sep-16 5:12 
GeneralMy vote of 5 Pin
Rahul Rajat Singh5-Feb-16 1:24
professionalRahul Rajat Singh5-Feb-16 1:24 
GeneralRe: My vote of 5 Pin
L. Michael6-Feb-16 10:05
memberL. Michael6-Feb-16 10:05 
Questionchatty Pin
Arne Christian Rosenfeldt5-Jan-16 21:01
memberArne Christian Rosenfeldt5-Jan-16 21:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web04 | 2.8.181207.3 | Last Updated 8 Jan 2018
Article Copyright 2016 by L. Michael
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid