【问题标题】:Versioning service layer with inheritance具有继承的版本控制服务层
【发布时间】:2021-06-23 06:47:07
【问题描述】:

我有一个 .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 上的版本控制服务层的想法?我愿意接受所有建议。

【问题讨论】:

    标签: c# api inheritance versioning api-versioning


    【解决方案1】:

    虽然您当然可以朝这个方向发展,但我不推荐它。 继承在我看来不是思考问题的正确方法。 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 cancellationToken) =>
         GetOne(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 cancellationToken)
       {
         var model = await repository.GetOne(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 次)。如图所示,您可以使用多种技术来减少控制器中的代码重复。

    【讨论】:

    • 实际上我不想在版本更改时一次又一次地编写方法。例如,我在 MobileAppServiceV1 中有一个 Get() 方法,当我创建新版本时,如果 Get() 方法可以正常工作,我不想要再写一次,这就是使用继承的原因。如果可能的话,你能用你的ide写几行我能更好理解的代码吗。谢谢你的意见:)
    • 当我在 v2 中删除 ListItem() 时,我仍然可以使用 v2,因为它将通过继承来自 v1。
    • "在查看源代码或调试时,您可能会跳过多个文件。甚至可能不清楚在哪里设置断点。" - 当有很多对象(存储库,服务,dto,..)时,我非常同意
    猜你喜欢
    • 2019-10-01
    • 2018-11-22
    • 2015-04-02
    • 1970-01-01
    • 1970-01-01
    • 2014-09-24
    • 2016-02-20
    • 2016-03-29
    • 2010-09-21
    相关资源
    最近更新 更多