Versioning APIs and Managing Code

Problem

We often face the issue like the integration was broken since there is a change on the other side of software or API. We want the software integrations to work regardless of changes in the interfaces. Backward compatibility is very crucial for every software should consider seriously. Backward compatibility is a property of an operating system, software, real-world product, or technology that allows for interoperability with an older legacy system, or with input designed for such a system. 

Modifying a system in a way that does not allow backward compatibility is sometimes called "breaking" backward compatibility. Such breaking usually incurs various types of costs, such as switching cost. The software is considered stable when its API that is used to invoke functions is stable across different versions. Any customers do not want to integrate with a software that keep changing its interface.

With microservices architecture, APIs are the key integration points where other microservices or systems are integrated. In order to maintain consistent API and support backward compatibility, the code should be structured in a certain way it will be easy to maintain and follow clean code principle.

Maintaining code for backward compatibility

Maintaining the code for backward compatibility is complex and error prone. A developer often faces issue like, how to maintain the source code for different version of API in a version control system? There are many strategies such as, maintain the source code is separate repositories for each version, or follow Gitflow model, where the source code is maintained is a different branch for each version of API etc.

The continuous integration and continuous deployment and with the trunk-based development, where a source code is maintained in single branch and there will be always a single source of truth. There is a constant flow of improvement is made to the system by constantly committing the code to git to the same branch again and again. We will explore the options of maintaining the source code in a single branch and support for backward compatibility.

Layered Architecture

To understand how to manage the source code in a single repo, it is also essential to understand the architecture we follow for a .Net solution. The layers allow us to manage the source code without affecting other layers and follow the principle of loosely coupled architecture. Each layer has their own models.

The architecture we follow is based on domain driven design and the solution has the below layers,

  1. API layer
  2. Domain layer
  3. Infrastructure layer

The below diagram shows the three different layers and the components associated within it.

Figure 1 - Layered Architecture

Let us understand more on the layers,

API Layer

The API layer contains the integration logic with other APIs. This layer has common functions like, authentication authorization, error handling, routing, configurations, versioning and API documentation etc. These modules or features are common across the services. This is the layer where we handle versioning of our endpoints.

Domain Layer

The domain layer represents the business domains or domain models. This layer handles business logic intended for the specific service. It is unique for every service and encapsulates the business logic regardless of the integration with other systems.

Infrastructure Layer

The infrastructure layer deals with the integration with downward systems. Like persisting data to databases, integration with other APIs, integration with message queue and file handling etc.

The Bigger Picture

To better understand the concept, let us assume we have an order service for order processing. We identify the initial model as ‘Order’ and versioned it as v1. It all good and deployed v1 to production. But after some time, a new business requirement suggests that we need to change our ‘Order’ model to include more properties and identify another model ‘OrderItem’ as well.

With the introduction of new changes, within our code we may end up changing in multiple places such as controllers, domain models etc.

We don’t want the versioning propagated throughout our solution. It will also become very difficult to maintain and becomes cumbersome. To avoid the complication, we want to keep the versioning to be handled only at the API Layer and transform the API model to Domain models.

The below diagram shows how the request is propagated through each layer, 

Figure 2 - Solution architecture for versioning API models

When a there is a system call for our api ‘/api/v1/order’ endpoint. The request invoke the v1 ‘OrderController’ housed in the ‘OrderService.Controller.v1’ Then we use the Automapper profiler ‘OrderService.Maps.v1.MappingProfile’. Same as v1, when we invoke ‘/api/v2/order’ endpoint, it is going to invoke v2 ‘OrderController’ that in turn call the ‘OrderService.Maps.v2.MappingProfile’. The mapping profile convert and transform the both the versions of API model into domain model.

The domain models can be mutated if there is any addition of member. The changes to the domain models are in complete control of a developer. The service can be compiled and deployed again. This approach will not affect any integrations that was done to previous versions.

Namespaces

Namespaces are used to organize the classes. It helps to control the scope of methods and classes in larger .Net programming projects. We use namespaces to segregate the controllers and models of different version. 

Folder Structure

Folders are used to organize the different versions of controllers and API models. 

There two level of folder structure are,

  1. Base folder
  2. Version folder

The base folder maintains all the base classes that is common regardless the versions. These classes are not changed often. This will avoid duplicating the controllers or module in all versions.

The version folder organized the version specific controllers or models. Every time a new version is introduced a new folder is created such as v1, v2, etc.

API Models

API Models servers as interface for consumers. The API models are versioned when there is a change in the members or properties. All models that are identified as common, which does not change in future can be a shared among the versions. Changes to the API models without versioning can directly affects the consumer integration endpoints.

In our example the API models are defined in OrderService.APIModels namespace under OrderService project.

The common models are placed at the base folder. For example, the ‘Address’ is defined as follows,

namespace OrderService.ApiModel
{
    public class Address
    {
        //Fields removed
    }
}

Under v1 folder our Order model is shown below,

namespace OrderService.ApiModel.v1
{
    public class Order
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public DateTime Date { get; set; }

    }
}

Under v2 folder the ‘Order’ model is changes as follows. Note, there is an addition of new field ‘Currency’.

namespace OrderService.ApiModel.v2
{
    public class Order
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public DateTime Date { get; set; }
        public string Currency { get; set; }
    }
}

Domain Models

The domain models represent business domain. Domain modes are in complete control of the developer. It can be mutated and deployed, which does not affect any of the consumer integration. Any strategies between the API versions can be handled within the domain models. It is important to keep the API models immutable. Once released we should not change the models in its lifetime.

In our sample example project the domain models are defined under the project ‘OrderService.Domain’

The unified domain model is shown below,

namespace OrderService.Domain
{
    public class Order
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public DateTime Date { get; set; }
        public string Currency { get; set; }
    }
}

Controllers

Each version of API has separate controllers. Controllers will transform the API models into domain models with the help Mapping Profile. Shared controllers is used by shared API models.

Shared ‘AddressController’ code is shown below,

namespace OrderService.Controllers
{
    [ApiVersion("1")]
    [ApiVersion("2")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [ApiController]
    public class AddressController : Controller
    {

Note, both the API version attribute is defined at the top.

The versioned controllers for ‘OrderController’ are placed under respective version folders. For example, ‘OrderController’ for v1 is shown below,

using OrderService.ApiModel.v1;
using OrderService.Application.Feature.Order.Commands;

namespace OrderService.Controllers.v1
{
    [ApiVersion("1")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [ApiController]
    public class OrderController : Controller
    {
        private readonly IMapper _mapper;
        private readonly IMediator _mediator;

        public OrderController(IMapper mapper, IMediator mediator)
        {
            _mapper = mapper;
            _mediator = mediator;
        }

        [MapToApiVersion("1")]
        [HttpGet("id")]
        [ProducesResponseType(typeof(Order), (int)HttpStatusCode.OK) ]
        public async Task<IActionResult> GetOrder([FromQuery] int orderId)
        {
           
            return Ok();
        }

        [MapToApiVersion("1")]
        [HttpPost]
        public async Task<IActionResult> PostOrder([FromBody]Order order)
        {
            var domainOrder = _mapper.Map<Domain.Order>(order);
            var result = _mediator.Send(new CreateOrderCommand { Order = domainOrder });
            var resultOrder = _mapper.Map<Order>(result.Result.Order);
            return Ok(resultOrder);
        }
    }
}

In a similar fashion the ‘OrderController’ for version 2 is placed under v2 folder.

Mapping Profiles

Mapping profiles transform models using Automapper. It transforms input model or API model to domain model. Input model represents the API model which changes between the versions. Domain model not to be versioned and can be mutated. Data received from each version of API model is transformed into domain models, thus the domain models or the business logic will be same regardless of new version of API models. 

The mapping profile for v1 ‘Order’ to domain model can be found at ‘OrderService.Maps.V1’ namespace,

namespace OrderService.Maps.v1
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            CreateMap<OrderService.ApiModel.v1.Order, OrderService.Domain.Order>().ReverseMap();
        }
    }
}

Automapper will take care of copying the member to member. Any member differs in name or type, or any conversion should handle explicitly.

Transformation Magic

Look at the Post method for both v1 and v2 ‘OrderController’s

V1 Post method,


        [MapToApiVersion("1")]
        [HttpPost]
        public async Task<IActionResult> PostOrder([FromBody]Order order)
        {
            var domainOrder = _mapper.Map<Domain.Order>(order);
            var result = _mediator.Send(new CreateOrderCommand { Order = domainOrder });
            var resultOrder = _mapper.Map<Order>(result.Result.Order);
            return Ok(resultOrder);
        }

V2 Post method,


        [MapToApiVersion("2")]
        [HttpPost]
        public async Task<IActionResult> PostOrder([FromBody] Order order)
        {
            var domainOrder = _mapper.Map<Domain.Order>(order);
            var result = _mediator.Send(new CreateOrderCommand { Order = domainOrder });
            var resultOrder = _mapper.Map<Order>(result.Result.Order);
            return Ok(resultOrder);
        }

You barely notice any change in the code except the attribute ‘MapToVersion’. All the magic happens within the mapping profile and the namespace declaration at the top of the code. Notice the namespace ‘OrderService.ApiModel.v1’ and ‘OrderService.ApiModel.v2’ declared at the top for both ‘OrderController’ respectively.

When the request is received the data is fetched from the corresponding ‘Order’ version model. The code _mapper.Map<Domain.Order>(order) will call the appropriate mapping profile.

Role of Command Handlers

The command handler is where the business logic for a domain is handled. The handlers were all in complete control of the developer.  The command handler does not change regardless of the change in ApiModels. The helps the keep the business logic to follow clean code.

In our example the command handler for processing order is placed at ‘OrderService.Application.Feature.Order.Create’ namespace.

The sample code is as follows,

namespace OrderService.Application.Feature.Order.Commands
{
    public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, CreateOrderCommandResponse>
    {
        public async Task<CreateOrderCommandResponse> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
        {
            var response = new CreateOrderCommandResponse { Id = Guid.NewGuid().ToString(), Message = "Order created successfully", Order = request.Order };
            response.Order.Id = response.Id;
            return response;
        }
    }
}

Summary

In this article I have portrayed how to handle he versioning of API at the code level by following clean code principle. However, API versioning can also manage at the deployment level. Containerization is a way to create an immutable build and deploy independently regardless of any change in input models, both the version of services can run parallelly.   The sample source code for the above can be downloaded from  https://github.com/arunvambur/manage-api-versions



Comments

Popular posts from this blog

Debugging and Testing Helm Charts Using VS Code

Handle Multipart Contents in Asp.Net Core

Validate appsettings in ASP.net Core using FluentValidation