【问题标题】:ASP.NET Web API Contract VersioningASP.NET Web API 合同版本控制
【发布时间】:2016-09-07 13:18:27
【问题描述】:

我们希望在接受标头中使用内容协商来实现基于版本的 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 版本 1 时,StudentV1 的实例应该在方法中传递,并且在版本 2 中也是如此。

有什么办法可以做到吗?

提前致谢!!

【问题讨论】:

  • 感谢@DanielStackenland!我们没有像 productType 这样的通用字段来识别发布的 JSON。此外,我们将有大约 50 - 70 个这样的类,例如 API 中的学生,稍后将在需要时进行版本化。
  • AbstractStudentController 的目的到底是什么?为什么不让 StudentV1Controller(和 V2)继承 AbstractBaseController 并使用 StudentV1(和 V2)作为参数?
  • @DanielStackenland- AbstractStudentController 用于声明所有版本的 API 合同。我们不能使用 StudentV1 和 StudentV2,因为不能在控制器合同中使用继承。主要目的是,如果我们在版本 1 中有 10 个 API 方法,并且如果只需要对一个 API 方法进行合约更改,那么只需要在版本 2 控制器中覆盖它,其他 9 个将被版本 2 继承。数据合约应基于反序列化该 API 的相应版本。如果需要,我们可以将具体类的 API 归因于 API,但不能在参数中。

标签: asp.net asp.net-mvc asp.net-web-api asp.net-web-api2 versioning


【解决方案1】:

ASP.NET API Versioning 能够实现您的目标。首先,您需要添加对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 问题的错误方法。 API 和 HTTP 都不支持继承。那是支持语言的实现细节,这也有点阻抗不匹配。一个关键问题是您不能取消继承方法,因此也不能继承 API。

如果你真的坚持继承。选择以下选项之一:

  • 只有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,因为没有标准格式。此外,您将必须更新配置中的 ASP.NET MediaTypeFormatter 映射。内置媒体类型映射不考虑媒体类型参数(例如v 参数没有影响)。

下表显示了映射:

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 版本,包括根本没有。如果您想在不需要媒体类型的情况下允许DELETE,这可能很有用。另一种选择是使用媒体类型和查询字符串版本控制方法。这将允许在查询字符串中为 DELETE API 指定 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"}

【讨论】:

    【解决方案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
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2018-02-21
      • 2017-03-18
      • 1970-01-01
      • 1970-01-01
      • 2021-06-19
      • 1970-01-01
      • 2014-10-11
      • 2016-07-17
      • 1970-01-01
      相关资源
      最近更新 更多