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