ASP.NET 网络 API 合同版本控制
ASP.NET Web API Contract Versioning
我们希望在接受 header 中使用内容协商实现基于版本 API。
我们能够通过一些继承和扩展默认的 HTTP 选择器来实现控制器和 API 方法。
使用以下示例代码实现控制器继承,
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; }
}
我们已经创建了上面的架构来减少版本变化的代码,假设版本 1 有 10 个 API 方法并且有一个 API 方法的变化比它应该可用的版本 2 代码不修改其他 9(它们是从版本 1 继承的)。
现在,我们面临的主要问题是合同版本控制,因为我们无法实例化抽象学生的实例。当有人发布 JSON 到 API 时,StudentV1 的版本 1 实例应该在方法中传递,在版本 2 中也是如此。
有什么办法可以实现吗?
提前致谢!!
根据您粘贴的代码,您可以使 AbstractStudentController 通用。
因为你声明的那些API抽象必须在每个API版本中实现,你可以用泛型定义类型。我希望我没有遗漏您的描述中的任何内容,因为您在 StudentV2Controller 中的实现中缺少 Patch,但被声明为抽象的。您想从 StudentV1Controller 派生 StudentV2Controller 吗?
public abstract class AbstractBaseController : ApiController
{
// common methods for all api
}
public abstract class AbstractStudentController<StudentType> : AbstractBaseController
{
// common methods for Student related API'sample
public abstract Post(StudentType student);
public abstract Patch(StudentType student);
}
public class StudentV1Controller : AbstractStudentController<StudentV1>
{
public override Post([FromBody]StudentV1 student) // student should be instance of StudentV1 from JSON
{
// To Do: Insert V1 Student
}
public override Patch([FromBody]StudentV1 student) // student should be instance of StudentV1 from JSON
{
// To Do: Patch V1 Student
}
}
public class StudentV2Controller : AbstractStudentController<StudentV2>
{
//
public override Post([FromBody]StudentV2 student) // student should be instance of StudentV2 from JSON
{
// To Do: Insert V2 Student
}
}
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 包。
然后您可以像这样配置您的应用程序:
public class WebApiConfig
{
public static void Configure(HttpConfiguration config)
{
config.AddApiVersioning(
options => options.ApiVersionReader = new MediaTypeApiVersionReader());
}
}
您的控制器可能看起来像:
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);
}
}
}
我强烈反对继承。这是可能的,但这是解决 IMO 问题的错误方法。 APIs 和 HTTP 都不支持继承。这是支持语言的实现细节,也有点阻抗不匹配。一个关键问题是您不能 取消继承 一个方法,因此,也不能 API.
如果你真的坚持继承。选择以下选项之一:
- 基地 class 只有
protected
个成员
- 将业务逻辑移出控制器
- 使用扩展方法或其他协作者来完成共享操作
例如,您可能做这样的事情:
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);
}
}
}
还有其他方法,但这是一个例子。如果您定义一个合理的版本控制策略(例如:N-2 版本),那么重复的数量是最小的。继承可能会导致比它解决的问题更多的问题。
当您按媒体类型进行版本化时,默认行为使用 v
媒体类型参数来指示 API 版本。如果您愿意,可以更改名称。其他形式的媒体类型版本控制是可能的(例如:application/json+student.v1
,你需要一个自定义的 IApiVersionReader 因为没有 standard格式。此外,您必须更新配置中的 ASP.NET MediaTypeFormatter 映射。内置媒体类型映射不考虑媒体类型参数(例如 v
参数没有影响)。
以下table显示映射:
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
是异常情况,因为它不需要输入或输出媒体类型。 Content-Type
将始终优先于 Accept
,因为正文需要它。 DELETE
API 可以设为 API version-neutral,这意味着将采用任何 API 版本,包括 none根本。如果您想在不需要媒体类型的情况下允许 DELETE
,这可能很有用。另一种选择是使用媒体类型和查询字符串版本控制方法。这将允许在 DELETE
APIs.
的查询字符串中指定 API 版本
通过网络,它看起来像:
请求
POST /student HTTP/2
Host: localhost
Content-Type: application/json;v=2.0
Content-Length: 37
{"firstName":"John","lastName":"Doe"}
回应
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"}
我们希望在接受 header 中使用内容协商实现基于版本 API。
我们能够通过一些继承和扩展默认的 HTTP 选择器来实现控制器和 API 方法。
使用以下示例代码实现控制器继承,
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; }
}
我们已经创建了上面的架构来减少版本变化的代码,假设版本 1 有 10 个 API 方法并且有一个 API 方法的变化比它应该可用的版本 2 代码不修改其他 9(它们是从版本 1 继承的)。
现在,我们面临的主要问题是合同版本控制,因为我们无法实例化抽象学生的实例。当有人发布 JSON 到 API 时,StudentV1 的版本 1 实例应该在方法中传递,在版本 2 中也是如此。
有什么办法可以实现吗?
提前致谢!!
根据您粘贴的代码,您可以使 AbstractStudentController 通用。 因为你声明的那些API抽象必须在每个API版本中实现,你可以用泛型定义类型。我希望我没有遗漏您的描述中的任何内容,因为您在 StudentV2Controller 中的实现中缺少 Patch,但被声明为抽象的。您想从 StudentV1Controller 派生 StudentV2Controller 吗?
public abstract class AbstractBaseController : ApiController
{
// common methods for all api
}
public abstract class AbstractStudentController<StudentType> : AbstractBaseController
{
// common methods for Student related API'sample
public abstract Post(StudentType student);
public abstract Patch(StudentType student);
}
public class StudentV1Controller : AbstractStudentController<StudentV1>
{
public override Post([FromBody]StudentV1 student) // student should be instance of StudentV1 from JSON
{
// To Do: Insert V1 Student
}
public override Patch([FromBody]StudentV1 student) // student should be instance of StudentV1 from JSON
{
// To Do: Patch V1 Student
}
}
public class StudentV2Controller : AbstractStudentController<StudentV2>
{
//
public override Post([FromBody]StudentV2 student) // student should be instance of StudentV2 from JSON
{
// To Do: Insert V2 Student
}
}
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 包。
然后您可以像这样配置您的应用程序:
public class WebApiConfig
{
public static void Configure(HttpConfiguration config)
{
config.AddApiVersioning(
options => options.ApiVersionReader = new MediaTypeApiVersionReader());
}
}
您的控制器可能看起来像:
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);
}
}
}
我强烈反对继承。这是可能的,但这是解决 IMO 问题的错误方法。 APIs 和 HTTP 都不支持继承。这是支持语言的实现细节,也有点阻抗不匹配。一个关键问题是您不能 取消继承 一个方法,因此,也不能 API.
如果你真的坚持继承。选择以下选项之一:
- 基地 class 只有
protected
个成员 - 将业务逻辑移出控制器
- 使用扩展方法或其他协作者来完成共享操作
例如,您可能做这样的事情:
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);
}
}
}
还有其他方法,但这是一个例子。如果您定义一个合理的版本控制策略(例如:N-2 版本),那么重复的数量是最小的。继承可能会导致比它解决的问题更多的问题。
当您按媒体类型进行版本化时,默认行为使用 v
媒体类型参数来指示 API 版本。如果您愿意,可以更改名称。其他形式的媒体类型版本控制是可能的(例如:application/json+student.v1
,你需要一个自定义的 IApiVersionReader 因为没有 standard格式。此外,您必须更新配置中的 ASP.NET MediaTypeFormatter 映射。内置媒体类型映射不考虑媒体类型参数(例如 v
参数没有影响)。
以下table显示映射:
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
是异常情况,因为它不需要输入或输出媒体类型。 Content-Type
将始终优先于 Accept
,因为正文需要它。 DELETE
API 可以设为 API version-neutral,这意味着将采用任何 API 版本,包括 none根本。如果您想在不需要媒体类型的情况下允许 DELETE
,这可能很有用。另一种选择是使用媒体类型和查询字符串版本控制方法。这将允许在 DELETE
APIs.
通过网络,它看起来像:
请求
POST /student HTTP/2
Host: localhost
Content-Type: application/json;v=2.0
Content-Length: 37
{"firstName":"John","lastName":"Doe"}
回应
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"}