Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Web API – A Solid Approach

4.84/5 (45 votes)
18 Nov 2021CPOL9 min read 98.4K   2.7K  
Easily create a lean Web API on top of a .NET library of stateless services
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 DomainServices project is also available as an open source project and published as a NuGet package. The sample code in this article uses this package.

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 UIs) 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. The code is made in Visual Studio 2022 using ASP.NET Core 6.0 and C# 10.

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 ProductsController class, with full CRUD-functionality:

C#
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    private readonly ProductService _productService;
 
    public ProductsController(IProductRepository productRepository)
    {
        _productService = new ProductService(productRepository);
    }
 
    [HttpGet]
    public ActionResult<IEnumerable<Product>> GetAll() => Ok(_productService.GetAll());
 
    [HttpGet("{id}")]
    public ActionResult<Product> Get(Guid id) => Ok(_productService.Get(id));
 
    [HttpPost]
    [ProducesResponseType(StatusCodes.Status201Created)]
    public ActionResult<Product> Add(ProductDto productDto)
    {
        var product = productDto.ToProduct();
        _productService.Add(product);
        return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
    }
 
    [HttpPut]
    public ActionResult<Product> Update(ProductDto productDto)
    {
        var product = productDto.ToProduct();
        _productService.Update(product);
        return Ok(_productService.Get(product.Id));
    }
 
    [HttpDelete("{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public IActionResult Delete(Guid id)
    {
        _productService.Remove(id);
        return NoContent();
    }
}

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 products controller: a number of very short public methods (actions) that essentially redirect the job to the underlying services.

If a controller action returns an IActionResult or ActionResult<T> the ASP.NET framework provides some underlying functionality to compose the HTTP response message. The framework comes with a number of built-in implementations of IActionResult - also known as convenience methods. For example, Ok which will create a response with HTTP status code 200 (OK) or CreatedAtAction which will create a response with status code 201 (Created) and a Location header with a route to the created resource.

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

C#
public class ProductsController : ControllerBase
{
    private readonly ProductService _productService;
 
    public ProductsController(IProductRepository productRepository)
    {
        _productService = new ProductService(productRepository);
    }

    ...
}

The dependency injection is configured in the Program.cs file:

C#
builder.Services.AddScoped<IProductRepository>(_ => productRepository);

Routing

When the web API receives an 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. This code uses attribute routing, which is the preferred option for REST APIs.

This is done using one of the route templates - for example, HttpGet. The below example defines the routing for an action retrieving a product with a specific ID:

C#
[HttpGet("api/products/{id}")]
public ActionResult<Product> Get(Guid id) => 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 action-specific route templates can be used in combination with a Route attribute on the controller, which defines a common routing prefix for all of the actions in a controller.

C#
[Route("api/products")]
public class ProductsController : ControllerBase
{
    ...
}

Model Binding

The process of mapping the variable parameters of an HTTP request to the action parameters is called model 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:

C#
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public IActionResult Delete(Guid id)
{
    _productService.Remove(id);
    return NoContent();
}

If you want to pass a more complex parameter, for example, a product when adding a new product, then you will have to read it from the body of the HTTP request. The framework automatically assumes that complex types are a part of the request body. This is done, for example, in the Add action of the product controller:

C#
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public ActionResult<Product> Add(ProductDto productDto)
{
    var product = productDto.ToProduct();
    _productService.Add(product);
    return CreatedAtAction(nameof(Get), new { id = 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

Model validation occurs automatically after model binding and reports errors where data does not conform to business rules – for example, if a negative product price is given.

In ASP.NET Core, 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:

C#
public struct ProductDto
{
    public Guid? Id { get; set; }
 
    [Required]
    [StringLength(100)]
    public string Name { get; set; }

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

    public Product ToProduct()
    {
        return new Product(Id ?? 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. If not provided, the ID will be automatically generated when calling the ToProduct method.

Exception Handling

The sample code also demonstrates how to create custom exception handling middleware:

C#
public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
 
    public ExceptionHandlingMiddleware(RequestDelegate next)
    {
        _next = next;
    }
 
    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await ex.ToHttpResponse(context);
        }
    }
}

If an exception is thrown, the ToHttpResponse extension method is called. In this method, selected exceptions are mapped to specific HTTP status codes. KeyNotFoundException and ArgumentOutOfRangeException are mapped to the HTTP status code 404 (Not Found) and ArgumentException is mapped to status code 400 (Bad Request). All other exceptions will be mapped to status code 500 (Internal Server Error).

C#
public static class ExtensionMethods
{
    public static Task ToHttpResponse(this Exception ex, HttpContext context)
    {
        HttpStatusCode code;
        if (ex is KeyNotFoundException || ex is ArgumentOutOfRangeException)
        {
            code = HttpStatusCode.NotFound;
        }
        else if (ex is ArgumentException)
        {
            code = HttpStatusCode.BadRequest;
        }
        else if (ex is NotImplementedException || ex is NotSupportedException)
        {
            code = HttpStatusCode.NotImplemented;
        }
        else
        {
            code = HttpStatusCode.InternalServerError;
        }
 
        var result = JsonSerializer.Serialize(new { error = ex.ToString() });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
 
    public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ExceptionHandlingMiddleware>();
    }
}

Now, adding the custom middleware to the request pipeline defined in Program.cs will enforce this policy, and ensure that proper HTTP status codes are returned when exceptions are thrown:

C#
app.UseExceptionHandling();

Debugging and Hosting

For demonstration purposes, an in-memory product repository, primed with a couple of products, is configured in the Program.cs file:

C#
var products = new List<Product> { 
    new Product(Guid.NewGuid(), "Coke") { Price = 1.35M },
    new Product(Guid.NewGuid(), "Fanta") { Price = 1.85M }
};

var productRepository = new InMemoryProductRepository(products);
builder.Services.AddScoped<IProductRepository>(_ => productRepository);

To debug the sample code in Visual Studio, set MyServices.Web as the startup project and press F5. This will fire up under the Swagger UI. You can now browse the API endpoints.

For example, to add a new product, expand the POST api/products endpoint, press the "Try it out"-button, modify the request body and press the "Execute" button.

Image 4

The web API can be deployed as any other ASP.NET web application - for example, it can be hosted on Windows using IIS or it can be published to 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 and a good foundation for production code, for clarity, many production code concerns are left out of this article and the sample code. These concerns include for example: security, versioning, caching, logging, content negotiation, etc. However, there are tons of valuable resources out there providing help on these subjects.

The DomainServices framework used in the sample code is available as an open source project and published as a NuGet package.

History

  • 5th January, 2016
    • Initial version
  • 7th January, 2016
    • Added a dependency graph diagram
    • Modified text
    • Modified code snippets syntax
  • 18th November, 2021
    • Sample code was updated to use ASP.NET Core 6.0 and C# 10
    • Now using ASP.NET Core built-in dependency injection instead of Unity
    • Introducing Swagger UI
    • Article text modified accordingly

License

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