Reputation: 14402
I'm having an issue with API Versioning (ASP.NET Core API 2.1). I'm trying to supplant one method of an existing controller without having to copy all of the methods in the previous version. I assumed this would work but it gives me a problem with routing conflicts. Example:
namespace MyApi.Controllers
{
[Produces("application/json")]
[Route("api/v{version:apiVersion}")]
public class BaseController : Controller
{
public string VersionNumber => GetRouteValue<string>(ControllerContext, "version");
protected static TValue GetRouteValue<TValue>(ControllerContext context, string name)
{
return (TValue)Convert.ChangeType(context.RouteData.Values[name], typeof(TValue));
}
}
}
namespace MyApi.Controllers
{
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ValuesController : BaseController
{
[HttpGet("values", Name = "GetValuesV1.0")]
public IActionResult GetValues() => Ok(new string[] { "value 1", "value 2" });
[HttpGet("values/{value}", Name = "GetValueV1.0")]
public IActionResult GetValue(string value) => Ok( value });
}
}
namespace MyApi.Controllers.V2_0
{
[ApiVersion("2.0")]
public class ValuesController : BaseController
{
[HttpGet("values", Name = "GetValuesV2.0")]
public IActionResult GetValues() => Ok(new string[] { "value 1", "value 2", "value 3" });
}
}
I then get the error:
The method 'Get' on path 'xxx' is registered multiple times.
I want to support the method GetValue(string value) in both versions, but I don't want to duplicate the code in the new controller every time I version. I just want to supplant one single method. Is this possible, or do I have to copy the entire previous controller and every method in it? This works, but feels horrible:
namespace MyApi.Controllers
{
[Produces("application/json")]
[Route("api/v{version:apiVersion}")]
public class BaseController : Controller
{
public string VersionNumber => GetRouteValue<string>(ControllerContext, "version");
protected static TValue GetRouteValue<TValue>(ControllerContext context, string name)
{
return (TValue)Convert.ChangeType(context.RouteData.Values[name], typeof(TValue));
}
}
}
namespace MyApi.Controllers
{
[ApiVersion("1.0")]
public class ValuesController : BaseController
{
[HttpGet("values", Name = "GetValuesV1.0")]
public IActionResult GetValues() => Ok(new string[] { "value 1", "value 2" });
[HttpGet("values/{value}", Name = "GetValueV1.0")]
public IActionResult GetValue(string value) => Ok( value });
}
}
namespace MyApi.Controllers.V2_0
{
[ApiVersion("2.0")]
public class ValuesController : BaseController
{
[HttpGet("values", Name = "GetValuesV2.0")]
public IActionResult GetValues() => Ok(new string[] { "value 1", "value 2", "value 3" });
[HttpGet("values/{value}", Name = "GetValueV2.0")]
public IActionResult GetValue(string value) => Ok( value });
}
}
This now works, but I've just duplicated code for no reason. In controllers with lots of code this just feels like code smell. Is there a workaround?
Upvotes: 3
Views: 1083
Reputation: 4368
Inheritance is a tricky thing and is arguably incongruent with REST or a Web API which has no concept of inheritance. The struggle, however, is real and what you really want to achieve is keeping the implementation DRY - completely fair. You didn't try to inherit API versions in your example, but I've seen that many times. I don't recommend it.
To understand why your first attempt didn't work and your second attempt did, you need to think about the URLs (e.g. identifiers). The issue is less about REST and more about how a framework like ASP.NET Core maps a HTTP request to code. The failure makes sense as 2 different implementations report that they handle the same request. This is ambiguous from a dispatching perspective.
In my opinion, controller (and web services in general) should not have business logic in it. The API serves as a HTTP facade to represent your business logic over the wire. Any logic in a controller should be limited to the context of HTTP. This can further be generalized by extension methods or base classes the provide common functionality (but not APIs). If your business logic is abstracted away in a - for lack of a better term - business layer, then the duplication is minimal. Each new version is a copy and paste of the old one with minimal changes that should be related to the API itself. I have seen the Copy/Paste/Replace method of creating new versions be effective.
Another option you have is to interleave versions within a controller. This can be yucky, but if you have very little change between versions, this is a workable compromise. Depending on how concerned you are about changing older code, you can always split things out later if you see things become untenable. Some service authors are adamant about not changing the code of older versions, which is understandable, but I feel is mitigated by sound test coverage. Using Client-Driven Contracts is another great method of verification.
The final thing to consider, and one that I think is largely overlooked, is what your versioning policy will be. Even without formally defining it, most service authors will have a policy of N-1 or N-2 in the back of their mind. If you have a well-defined policy, that will be an indicator as to how much baggage duplicate code will be when creating new versions. For example, if your policy is N-2, is it really so bad that some controller-level bits are duplicated up to 3 times? While we'd ideally like no duplication, attempting to refactor out this last little bit after we've generalized everything else that we could is likely not worth the effort.
I hope you find that insightful. I'm happy to elaborate further.
Upvotes: 2