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"}