Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Web API Contract Versioning

We would like to achieve version based API using content negotiation in accept header.

We are able to achieve for controller & API methods with some inheritance and extending the default HTTP selector.

Controller inheritance is achieved using following sample code,

public abstract class AbstractBaseController : ApiController
{
    // common methods for all api
}

public abstract class AbstractStudentController : AbstractBaseController
{
    // common methods for Student related API'sample

    public abstract Post(Student student);
    public abstract Patch(Student student);
}

public class StudentV1Controller : AbstractStudentController
{
    public override Post([FromBody]Student student) // student should be instance of StudentV1 from JSON
    {
        // To Do: Insert V1 Student
    }

    public override Patch([FromBody]Student student) // student should be instance of StudentV1 from JSON
    {
        // To Do: Patch V1 Student
    }
}

public class StudentV2Controller : AbstractStudentController
{
    // 
    public override Post([FromBody]Student student) // student should be instance of StudentV2 from JSON
    {
        // To Do: Insert V2 Student
    }
}

public abstract class Student
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class StudentV1 : Student
{   
}

public class StudentV2 : Student
{   
    public string Email { get; set; }
}

We have created above architecture to do less code with change in version, say if version 1 has 10 API methods and there is a change in one API method than it should be available in version 2 code without modifying other 9(they are inherited from version 1).

Now, the main problem we are facing is in contract versioning as we cannot instantiate an instance of an abstract student. When someone is posting JSON to API version 1 instance of StudentV1 should be passed in methods and same in version 2.

Is there any way to achieve this?

Thanks in advance!!

like image 607
Vijay Chauhan Avatar asked Nov 08 '22 09:11

Vijay Chauhan


1 Answers

ASP.NET API Versioning is capable of achieving your goals. First, you'll want to add a reference to the ASP.NET Web API API Versioning NuGet package.

You would then configure your application something like:

public class WebApiConfig
{
   public static void Configure(HttpConfiguration config)
   {
       config.AddApiVersioning(
          options => options.ApiVersionReader = new MediaTypeApiVersionReader());
   }
}

Your controllers might look something like:

namespace MyApp.Controllers
{
    namespace V1
    {
        [ApiVersion("1.0")]
        [RoutePrefix("student")]
        public class StudentController : ApiController
        {
            [Route("{id}", Name = "GetStudent")]
            public IHttpActionResult Get(int id) =>
                Ok(new Student() { Id = id });

            [Route]
            public IHttpActionResult Post([FromBody] Student student)
            {
                student.Id = 42;
                var location = Link("GetStudent", new { id = student.Id });
                return Created(location, student);
            }

            [Route("{id}")]
            public IHttpActionResult Patch(int id, [FromBody] Student student) =>
                Ok(student);
        }
    }

    namespace V2
    {
        [ApiVersion("2.0")]
        [RoutePrefix("student")]
        public class StudentController : ApiController
        {
            [Route("{id}", Name = "GetStudentV2")]
            public IHttpActionResult Get(int id) =>
                Ok(new Student() { Id = id });

            [Route]
            public IHttpActionResult Post([FromBody] StudentV2 student)
            {
                student.Id = 42;
                var location = Link("GetStudentV2", new { id = student.Id });
                return Created(location, student);
            }

            [Route("{id}")]
            public IHttpActionResult Patch(int id, [FromBody] StudentV2 student) =>
                Ok(student);
        }
    }
}

I strongly advise against inheritance. It's possible, but is the wrong approach to the problem IMO. Neither APIs nor HTTP support inheritance. That is an implementation detail of the backing language, which is also somewhat of an impedance mismatch. A key problem is that you cannot uninherit a method and, hence, nor an API.

If you really insist on inheritance. Choose one of the following options:

  • Base class with only protected members
  • Move business logic out of the controllers
  • Use extension methods or other collaborators to fulfill shared operations

For example, you might do something like this:

namespace MyApp.Controllers
{
    public abstract class StudentController<T> : ApiController
        where T: Student
    {
        protected virtual IHttpActionResult Get(int id)
        {
            // common implementation
        }

        protected virtual IHttpActionResult Post([FromBody] T student)
        {
            // common implementation
        }

        protected virtual IHttpActionResult Patch(int id, [FromBody] Student student)
        {
            // common implementation
        }
    }

    namespace V1
    {
        [ApiVersion("1.0")]
        [RoutePrefix("student")]
        public class StudentController : StudentController<Student>
        {
            [Route("{id}", Name = "GetStudentV1")]
            public IHttpActionResult Get(int id) => base.Get(id);

            [Route]
            public IHttpActionResult Post([FromBody] Student student) =>
                base.Post(student);

            [Route("{id}")]
            public IHttpActionResult Patch(int id, [FromBody] Student student) =>
                base.Patch(student);
        }
    }

    namespace V2
    {
        [ApiVersion("2.0")]
        [RoutePrefix("student")]
        public class StudentController : StudentController<StudentV2>
        {
            [Route("{id}", Name = "GetStudentV2")]
            public IHttpActionResult Get(int id) => base.Get(id);

            [Route]
            public IHttpActionResult Post([FromBody] StudentV2 student) =>
                base.Post(student);

            [Route("{id}")]
            public IHttpActionResult Patch(int id, [FromBody] StudentV2 student) =>
                base.Patch(student);
        }
    }
}

There are other ways, but that is one example. If you define a sensible versioning policy (ex: N-2 versions), then the amount of duplication is minimal. Inheritance will likely cause more problems than it solves.

When you version by media type, the default behavior uses the v media type parameter to indicate the API version. You can change name if you wish. Other forms of versioning by media type are possible (ex: application/json+student.v1, you'd need a custom IApiVersionReader as there is no standard format. In addition, you'll have to update the ASP.NET MediaTypeFormatter mappings in the configuration. The built-in media type mapping does not consider media type parameters (e.g. the v parameter has no impact).

The following table shows the mapping:

Method Header Example
GET Accept application/json;v=1.0
PUT Content-Type application/json;v=1.0
POST Content-Type application/json;v=1.0
PATCH Content-Type application/json;v=1.0
DELETE Accept or Content-Type application/json;v=1.0

DELETE is an outlier case as it doesn't require a media type in or out. Content-Type will always take precedence over Accept because it is required for the body. A DELETE API can be made API version-neutral, meaning will take any API version, including none at all. This may be useful if you want to allow DELETE without requiring a media type. Another alternative could be to use media type and query string versioning methods. This would allow specifying the API version in the query string for DELETE APIs.

Over the wire, it will look like:

Request

POST /student HTTP/2
Host: localhost
Content-Type: application/json;v=2.0
Content-Length: 37

{"firstName":"John","lastName":"Doe"}

Response

HTTP/2 201 Created
Content-Type: application/json;v=2.0
Content-Length: 45
Location: http://localhost/student/42

{"id":42,"firstName":"John","lastName":"Doe"}
like image 56
Chris Martinez Avatar answered Dec 10 '22 17:12

Chris Martinez