ASP.NET Web API 合同版本控制

5
我们希望通过在accept头部使用内容协商来实现基于版本的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继承而来)。
现在,我们面临的主要问题是合同版本控制,因为我们不能实例化抽象学生的实例。当有人向API版本1发布JSON时,应在方法中传递StudentV1的实例,并在版本2中保持相同。
是否有任何方法可以实现这一点?
提前感谢!!

3
看起来是这样的:https://dev59.com/pGEi5IYBdhLWcg3wZ7q8 - Daniel Stackenland
感谢@DanielStackenland!!我们没有像productType这样的公共字段来识别发布的JSON。此外,我们将在API中拥有约50-70个类似于student的类,以后需要进行版本控制。 - Vijay Chauhan
AbstractStudentController的目的是什么呢?为什么不让StudentV1Controller(和V2)继承AbstractBaseController并使用StudentV1(和V2)作为参数呢? - Daniel Stackenland
@DanielStackenland- AbstractStudentController 用于声明所有版本 API 的契约。我们不能使用 StudentV1 和 StudentV2,因为控制器合同中不能使用继承。主要目的是,如果在版本 1 中有 10 个 API 方法,如果只需要更改一个 API 方法的契约,则只需在版本 2 控制器中重写它,其他 9 个将被继承到版本 2。数据契约应基于相应 API 版本进行反序列化。如果需要,我们可以针对具体类属性 API,但不可在参数中。 - Vijay Chauhan
2个回答

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);
        }
    }
}

我强烈建议不要使用继承。虽然这是可能的,但在我的看法中,这是解决问题的错误方法。API和HTTP都不支持继承。那是后备语言的实现细节,也有点阻抗不匹配。一个关键问题是你无法取消继承一个方法,因此也无法取消继承API。
如果你真的坚持使用继承,请选择以下选项之一:
1.只有受保护成员的基类 2.将业务逻辑从控制器中移出 3.使用扩展方法或其他协作者来执行共享操作
例如,你可以像这样做:
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 参数没有影响)。

下表显示了映射:

方法 头部信息 示例
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 AcceptContent-Type application/json;v=1.0

DELETE是一个特例,因为它不需要输入或输出媒体类型。由于必须提供请求主体,因此Content-Type始终优先于Accept。可以使DELETE API成为API版本中立,这意味着将接受任何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"}

0
根据您贴出的代码,您可以将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
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接