具有继承的版本控制服务层 变体 1变体 2变体 3结论

问题描述

我有一个 .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());
        }

    }

enter image description here

例如,在 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 次)。正如所展示的,您可以使用多种技术来减少控制器内的代码重复。