问题描述
我有一个 .net core 3.1 api,我想对我的控制器进行版本控制,我认为在服务层上有一些版本控制结构,如下所示
public interface IVersionableObject { }
public class GetDataV1 : IVersionableObject { }
public class PostDataV1 : IVersionableObject { }
public class GetDataV2 : IVersionableObject { }
public class PostDataV2 : IVersionableObject { }
public class ListItemV1 : IVersionableObject { }
public class MobileAppServiceV1
{
public virtual async Task<IVersionableObject> Get()
{
return new GetDataV1();
}
public virtual async Task<IVersionableObject> Post()
{
return new PostDataV1();
}
public virtual async Task<IVersionableObject> ListItems()
{
return new ListItemV1();
}
}
public class MobileAppServiceV2 : MobileAppServiceV1
{
public override async Task<IVersionableObject> Get()
{
return new GetDataV2();
}
public override async Task<IVersionableObject> Post()
{
return new PostDataV2();
}
[Obsolete("This method is not available for after V1",true)]
public async Task<IVersionableObject> ListItems()
{
throw new NotSupportedException("This method is not available for after V1");
}
}
让我们检查控制器
V1 控制器
[ApiVersion("1.0")]
[Route("api/{v:apiVersion}/values")]
public class ValuesControllerV1 : ControllerBase
{
private readonly MobileAppServiceV1 _mobileAppServiceV1;
public ValuesControllerV1()
{
_mobileAppServiceV1 = new MobileAppServiceV1();
}
[HttpGet]
public async Task<IActionResult> Get()
{
return Ok(await _mobileAppServiceV1.Get());
}
[HttpGet("listItem")]
public async Task<IActionResult> ListItems()
{
return Ok(await _mobileAppServiceV1.ListItems());
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] string value)
{
return Ok(await _mobileAppServiceV1.Post());
}
}
V2 控制器
[ApiVersion("2.0")]
[Route("api/{v:apiVersion}/values")]
public class ValuesControllerV2 : ControllerBase
{
private readonly MobileAppServiceV2 _mobileAppServiceV2;
public ValuesControllerV2()
{
_mobileAppServiceV2 = new MobileAppServiceV2();
}
[HttpGet]
public async Task<IActionResult> Get()
{
return Ok(await _mobileAppServiceV2.Get());
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] string value)
{
return Ok(await _mobileAppServiceV2.Post());
}
}
例如,在 v2 上删除了 ListItems 方法,我避免在 v2 上使用带有 Obselete
属性的 ListItem 方法。
最后,我认为结构是这样的,我尝试用示例代码来展示它。您能否就 Web api 上的版本控制服务层提供一些关于这种结构是否良好的想法?我愿意接受所有建议。
解决方法
虽然您当然可以朝这个方向发展,但这不是我推荐的。 继承在我看来并不是思考问题的正确方式。 HTTP 没有继承的概念。要使其发挥作用,存在许多问题和挑战。如果您的目标是共享通用代码,那么您还有其他几种选择,例如:
- 继承
protected
不是动作的方法;适当地使用public
操作公开它们 - 将尽可能多的非 HTTP 逻辑委派给控制器之外的代码
- 使用扩展方法或其他类型的扩展来共享内部控制器逻辑
[Obsolete]
属性不会执行您希望它执行的操作。虽然它确实会导致编译错误,如图所示,为什么不直接删除该方法呢?唯一的极端情况是如果您跨多个程序集进行继承,这会更加复杂。如果完全删除原始代码不是一种选择,那么更好的方法是使用 [NonAction]
修饰过时的方法,使其不再对 ASP.NET 可见。
变体 1
使用受保护的方法来共享逻辑。
[ApiController]
public abstract class ApiController : ControllerBase
{
protected async virtual Task<IActionResult> GetAll(CancellationToken cancellationToken)
{
// TODO: implementation
await Task.Yield();
return Ok();
}
protected async virtual Task<IActionResult> GetOne(int id,CancellationToken cancellationToken)
{
// TODO: implementation
await Task.Yield();
return Ok();
}
}
[ApiVersion("1.0")]
[Route("[controller]")]
public class MobileController : ApiController
{
[HttpGet("list")]
public Task<IActionResult> Get(CancellationToken cancellationToken) =>
GetAll(cancellationToken);
[HttpGet("{id}")]
public Task<IActionResult> Get(int id,CancellationToken cancellationToken) =>
GetOne(id,cancellationToken);
}
[ApiVersion("2.0")]
[Route("[controller]")]
public class Mobile2Controller : ApiController
{
[HttpGet("list")]
public Task<IActionResult> Get(CancellationToken cancellationToken) =>
GetAll(cancellationToken);
[HttpGet("{id:int}")] // new route constraint,but could be alt implementation
public Task<IActionResult> Get(int id,cancellationToken);
}
变体 2
将非 API 逻辑移出控制器。
public interface IRepository<T>
{
IAsyncEnumerable<T> GetAll(CancellationToken cancellationToken);
Task<T> GetOne(int id,CancellationToken cancellationToken);
}
// TODO: implement IRepository<T>
// NOTE: a generic base class 'could' be used for common logic
[ApiVersion("1.0")]
[ApiController]
[Route("[controller]")]
public class MobileController : ControllerBase
{
readonly IRepository<MobileApp> repository;
public MobileController(IRepository<MobileApp> repository) =>
this.repository = repository;
[HttpGet("list")]
public IAsyncEnumerable<MobileApp> Get(CancellationToken cancellationToken) =>
repository.GetAll(cancellationToken);
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id,CancellationToken cancellationToken)
{
var model = await repository.GetOne(id,cancellationToken);
if (model == null)
{
return NotFound();
}
return Ok(model);
}
}
[ApiVersion("2.0")]
[ApiController]
[Route("[controller]")]
public class Mobile2Controller : ControllerBase
{
readonly IRepository<MobileApp2> repository;
public Mobile2Controller(IRepository<MobileApp2> repository) =>
this.repository = repository;
[HttpGet("list")]
public IAsyncEnumerable<MobileApp2> Get(CancellationToken cancellationToken) =>
repository.GetAll(cancellationToken);
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id,cancellationToken);
if (model == null)
{
return NotFound();
}
return Ok(model);
}
}
变体 3
使用属性忽略旧方法。我不建议这样做,因为随着时间的推移,它会使维护变得混乱。
[ApiVersion("1.0")]
[ApiController]
[Route("[controller]")]
public class MobileController : ControllerBase
{
[HttpGet("list")]
public virtual async Task<IActionResult> Get(CancellationToken cancellationToken)
{
await Task.Yield();
return Ok();
}
[HttpGet("{id}")]
public virtual async Task<IActionResult> Get(int id,CancellationToken cancellationToken)
{
await Task.Yield();
return Ok();
}
}
[ApiVersion("2.0")]
[Route("[controller]")]
public class Mobile2Controller : MobileController
{
[NonAction] // exclude method as action
public override Task<IActionResult> Get(int id,CancellationToken cancellationToken) =>
Task.FromResult<IActionResult>(Ok());
[HttpGet("{id:guid}")]
public virtual async Task<IActionResult> GetV2(Guid id,CancellationToken cancellationToken)
{
await Task.Yield();
return Ok();
}
}
结论
这些只是几种可能性,但还有其他可能性。随着时间的推移,继承往往会使事情变得不清楚并且更难管理。您需要知道/记住哪些属性是继承的,哪些不是。在查看源代码或调试时,您可能会跳过多个文件。甚至可能不清楚在哪里设置断点。
您应该预先定义一个众所周知的版本控制策略。一个常见的策略是 N-2 个版本。如果您坚持该政策,那么复制控制器源的想法并不是那么糟糕(例如 3 次)。正如所展示的,您可以使用多种技术来减少控制器内的代码重复。