Vlad's blog

In programming veritas

Web API versioning in real world applications

leave a comment »

Once you have published your API it is set in stone. Customers who use your API expect it to not change. Otherwise their code may fail due to changed contract or behavior. But requirements will change and we need to figure out a way to evolve API without breaking existing clients. So every time when a breaking API change occurs you need to release a new version of API. That does not mean you need to support all versions forever. But you need to get rid of old versions of API with some care so customers have an idea how to move to a newer version of API.
In this post I am going to discuss how Web API versioning affects entire application and provide some recommendations how to organize source code to maintain different versions of API.

Example project can be downloaded here.

Introducing test application

In order to illustrate API versioning strategies we are going to develop a simple Web API for retrieving tasks. A task contains Name, Description, Owner and task state attributes: IsStarted and IsFinished. Each task belongs to particular Project which is barely a container of tasks. Application consists of three layers:

  • Domain layer that includes Task and Project definition
  • Data access layer that defines TaskRepository class. TaskRepository uses domain classes in serialization.
  • Distributes services layer defines DTO’s used by TaskController

versioning-domain

TaskController declares method Get(int id) which is entry point for Web API. So the following HTTP request will return task object which has id equal 1.
http://localhost:61392/api/tasks/1

What is a breaking change

Changes to the contract of an API are considered as a breaking change. Changes that impact the backwards compatibility of an API are breaking changes. Definition of backward compatibility may vary depending on business needs. For instance Microsoft treats the addition of new field in a response in Azure as not backwards compatible.
Clear examples of breaking changes:

  • Removing or renaming APIs or API parameters
  • Changes in behavior for an existing API
  • Changes in Error Codes and Fault Contracts
  • Changes in resources data structure

Few words about API versioning formats

There are four common formats used to version your API:

  • URI path segment versioning
  • Query string parameter versioning
  • Versioning with Content Negotiation
  • Versioning with request headers

In this post I am not going to discuss pros and cons of each format because this subject requires a separate post. My personal preference is URI path segment versioning which I used in the examples. I used ASP.NET API Versioning library developed by Microsoft (https://github.com/Microsoft/aspnet-api-versioning).

Solution structure

I am going to to discuss two types of changes:

  • Change in resources data structure
  • Changes in behavior for an existing API

It’s important to mention that changes affect all layers of application not only Web API controller. For instance, if we decided to add a new field in TaskDto class we need to support two version of Web API: the new one and previous that returns old data structure. That essentially means we need to have two versions of TaskRepository. Also we need to declare two Task domain classes for each version. So versioning is a cross cutting concern that imposes some requirements on solution structure.
Domain entities and repositories are placed in folders that belong to particular version, i.e. v1_0 corresponds to version 1.0, v1_1 to version 1.1 etc.

api-versioning-domain

The same is for controllers (v1_0_v1_1 means that controller supports two versions: version 1.0 and version 1.1)

api-versioning-controller

and DTO’s

api-versioning-dtos

Web API: version 1.0

We start our API project by defining domain classes, repository, DTO’s and controller.

public class Task
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Owner { get; set; }
    public Project Project { get; set; }
    public bool IsStarted { get; set; }
    public bool IsFinished { get; set; }
}

public class TaskRepository
{
    public Entities.v1_0.Task Find(int id)
    {
        Entities.v1_0.Project project = new Entities.v1_0.Project
        {
            Id = 1,
            Name = "Project 1",
            Description = "Project 1 description"
        };

        var task = new Entities.v1_0.Task
        {
            Id = id,
            Name = $"Task {id} name",
            Description = $"Task {id} description",
            Owner = "User 1",
            Project = project,
            IsStarted = true,
            IsFinished = false,
        };

        return task;
    }
}

public class TaskDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Owner { get; set; }
    public int ProjectId { get; set; }
    public string ProjectName { get; set; }
    public bool IsStarted { get; set; }
    public bool IsFinished { get; set; }
}

[ApiVersion("1.0")]
public class TaskController : ApiController
{
    private TaskRepository taskRepo = new TaskRepository();

    [Route("api/v{version:apiVersion}/tasks/{id}")]
    [MapToApiVersion("1.0")]
    public Dtos.v1_0.TaskDto Get(int id)
    {
        Task task = taskRepo.Find(id);

        return new Dtos.v1_0.TaskDto
        {
            Id = task.Id,
            Name = task.Name,
            Owner = task.Owner,
            IsStarted = task.IsStarted,
            IsFinished = task.IsFinished,
            ProjectId = task.Project.Id,
            ProjectName = task.Project.Name
        };
    }
}

The following HTTP request must be executed in order to retrieve a task. Note that we use URI path segment versioning where version becomes a part of URI.
http://localhost:61392/api/v1.0/tasks/1

Web API: version 1.1

After some thinking we decided to replace two fields Task.IsStarted and Task.IsFinished with more appropriate field Task.State so domain object has been changed.

public class Task
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Owner { get; set; }
    public v1_0.Project Project { get; set; }
    public int State { get; set; }
}

This change obviously affected DTO.

public class TaskDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Owner { get; set; }
    public int ProjectId { get; set; }
    public string ProjectName { get; set; }
    public string ProjectDescription { get; set; }
    public int State { get; set; }
}

At this moment we can either create new controller or add a new method for getting task of version 1.1. I prefer to add new method since it seems more convenient. So now we have Dtos.v1_0.TaskDto GetV1_0(int id) for retrieving Task version 1.0 and method Dtos.v1_1.TaskDto GetV1_1(int id) for retrieving task version 1.1. Also we have to add a second repository class.

[ApiVersion("1.0")]
[ApiVersion("1.1")]
public class TaskController : ApiController
{
    private Domain.Repositories.v1_0.TaskRepository taskRepoV1_0 = new Domain.Repositories.v1_0.TaskRepository();
    private Domain.Repositories.v1_1.TaskRepository taskRepoV1_1 = new Domain.Repositories.v1_1.TaskRepository();

    [Route("api/v{version:apiVersion}/tasks/{id}")]
    [MapToApiVersion("1.0")]
    public Dtos.v1_0.TaskDto GetV1_0(int id)
    {
        Task task = taskRepoV1_0.Find(id);

        return new Dtos.v1_0.TaskDto
        {
            Id = task.Id,
            Name = task.Name,
            Owner = task.Owner,
            IsStarted = task.IsStarted,
            IsFinished = task.IsFinished,
            ProjectId = task.Project.Id,
            ProjectName = task.Project.Name
        };
    }

    [Route("api/v{version:apiVersion}/tasks/{id}")]
    [MapToApiVersion("1.1")]
    public Dtos.v1_1.TaskDto GetV1_1(int id)
    {
        Domain.Entities.v1_1.Task task = taskRepoV1_1.Find(id);

        return new Dtos.v1_1.TaskDto
        {
            Id = task.Id,
            Name = task.Name,
            Owner = task.Owner,
            State = task.State,
            ProjectId = task.Project.Id,
            ProjectName = task.Project.Name,
            ProjectDescription = task.Project.Description
        };
    }
}

Two versions of API are now supported:
http://localhost:61392/api/v1.0/tasks/1
http://localhost:61392/api/v1.1/tasks/1

Web API: version 2.0

When the system is getting mature we started thinking about performance and maintenance. So we decided to follow modern micro services architecture and splitted the implementation into two micro serices: Task Service and Project Service. Instead of accessing the database directly from TaskManager API we call two external Web services: Task Service and Project service. Then combine the results into DTO. DTO has not changed but we still have to release a new version since this is a clear example of change in behavior which is considered as breaking change.
We plan to drastically change TaskController so we can’t add a new method to serve version 2.0, instead we need to create a completely new controller. This controller is no longer based on TaskRepository, instead it has two new dependencies – TaskServiceClient and ProjectServiceClient.

[ApiVersion("2.0")]
public class TaskController: ApiController
{
    private DistributedServices.v2_0.TaskServiceClient taskServiceClient = new DistributedServices.v2_0.TaskServiceClient();
    private DistributedServices.v2_0.ProjectServiceClient projectServiceClient = new DistributedServices.v2_0.ProjectServiceClient();

    [Route("api/v{version:apiVersion}/tasks/{id}")]
    public async Task<Dtos.v1_1.TaskDto> Get(int id)
    {
        DistributedServices.v2_0.Dtos.TaskDto task = await taskServiceClient.GetTask(id);
        DistributedServices.v2_0.Dtos.ProjectDto project = await projectServiceClient.GetProject(task.ProjectId);

        return new Dtos.v1_1.TaskDto
        {
            Id = id,
            Name = task.Name,
            State = task.State,
            Owner = task.Owner,
            ProjectDescription = project.Description,
            ProjectId = task.ProjectId,
            ProjectName = project.Name
        };
    }
}

So now application supports three versions of API:
http://localhost:61392/api/v1.0/tasks/1
http://localhost:61392/api/v1.1/tasks/1
http://localhost:61392/api/v2.0/tasks/1

Conclusion

Web API versioning is a good example of cross cutting concern that affects almost every layer of your application: domain, data access, distributed services. The important thing here is to think about versioning at the very beginning and come up with some directory structures and naming conventions which will be clear for all members of your team.

Advertisements

Written by vsukhachev

December 12, 2016 at 10:37 am

Posted in Development

Tagged with

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: