【问题标题】:How to version Web API using Namespaces - ~/api/v1/products如何使用命名空间对 Web API 进行版本控制 - ~/api/v1/products
【发布时间】:2016-10-21 09:44:19
【问题描述】:

我正在尝试制作一个版本化的 API,所以基本上我遵循了这篇文章:https://blogs.msdn.microsoft.com/webdev/2013/03/07/asp-net-web-api-using-namespaces-to-version-web-apis/

我已经为我的 WebApi 设置了一些路由,如下所示:

namespace WebApiApplication {
public static class WebApiConfig {
    public static void Register(HttpConfiguration config) {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{version}/{controller}",
            defaults: new { version = "v2" }
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApiWithId",
            routeTemplate: "api/{version}/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(config));
    }
}}

然后我的项目有一个 Controllers 文件夹,其中包含两个文件夹“v1”和“v2”。控制器看起来像这样:

~/Controllers/v1/ProductsController.cs

namespace WebApiApplication.Controllers.v1 {
[RoutePrefix("api/v1/products")]
public class ProductsController : ApiController {
    Product[] products = new Product[] {
        new Product { UserReference = 1, Name = "product1" },
        new Product { UserReference = 2, Name = "product2" }
    };

    [HttpGet]
    [Route("")]
    public IEnumerable<Product> GetAllProducts() {
        return products;
    }

    [HttpGet]
    [Route("{userReference}")]
    public IHttpActionResult GetProducts(int userReference) {
        var res = products.Where(t => t.UserReference == userReference);

        if (res == null)
            return NotFound();

        return Ok(res);
    }
}}

~/Controllers/v2/ProductsController.cs

namespace WebApiApplication.Controllers.v2 {
[RoutePrefix("api/v2/products")]
public class ProductsController : ApiController {
    Product[] products = new Product[] {
        new Product { UserReference = "a", Name = "product1" },
        new Product { UserReference = "b", Name = "product2" }
    };

    [HttpGet]
    [Route("")]
    public IEnumerable<Product> GetAllProducts() {
        return products;
    }

    [HttpGet]
    [Route("{userReference}")]
    public IHttpActionResult GetProducts(string userReference) {
        var res = products.Where(t => t.UserReference == userReference);

        if (res == null)
            return NotFound();

        return Ok(res);
    }
}}

这里版本之间的唯一区别是 UserReference 在 V2 中变成了一个字符串。

因为我在两个版本中都有相同的控制器名称,所以我必须覆盖当前的“IHttpControllerSelector”,才能找到请求的控制器:

~/NamespaceHttpControllerSelector.cs

public class NamespaceHttpControllerSelector : IHttpControllerSelector {
    private const string VERSION_KEY = "version";
    private const string CONTROLLER_KEY = "controller";

    private readonly HttpConfiguration _configuration;
    private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
    private readonly HashSet<string> _duplicates;

    public NamespaceHttpControllerSelector(HttpConfiguration configuration) {
        _configuration = configuration;
        _duplicates = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
    }

    public HttpControllerDescriptor SelectController(HttpRequestMessage request) {
        IHttpRouteData routeData = request.GetRouteData();

        if (routeData == null)
            throw new HttpResponseException(HttpStatusCode.NotFound);

        string versionName = GetRouteVariable<string>(routeData, VERSION_KEY); // Here, I am always getting null

        if (versionName == null)
            throw new HttpResponseException(HttpStatusCode.NotFound);

        string controllerName = GetRouteVariable<string>(routeData, CONTROLLER_KEY);

        if (controllerName == null)
            throw new HttpResponseException(HttpStatusCode.NotFound);

        // Find a matching controller.
        string key = string.Format(CultureInfo.InvariantCulture, "{1}.{2}", versionName, controllerName);

        HttpControllerDescriptor controllerDescriptor;
        if (_controllers.Value.TryGetValue(key, out controllerDescriptor)) {
            return controllerDescriptor;
        }
        else if (_duplicates.Contains(key)) {
            throw new HttpResponseException(
                request.CreateErrorResponse(
                    HttpStatusCode.InternalServerError,
                    "Multiple controllers were found that match this request."));
        }
        else {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
    }

    public IDictionary<string, HttpControllerDescriptor> GetControllerMapping() {
        return _controllers.Value;
    }

    private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary() {
        var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);

        // Create a lookup table where key is "namespace.controller". The value of "namespace" is the last
        // segment of the full namespace. For example:
        // MyApplication.Controllers.V1.ProductsController => "V1.Products"
        IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
        IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();

        ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);

        foreach (Type t in controllerTypes) {
            var segments = t.Namespace.Split(Type.Delimiter);

            // For the dictionary key, strip "Controller" from the end of the type name.
            // This matches the behavior of DefaultHttpControllerSelector.
            var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);

            var key = string.Format(
                CultureInfo.InvariantCulture,
                "{0}.{1}",
                segments[segments.Length - 1],
                controllerName);

            // Check for duplicate keys.
            if (dictionary.Keys.Contains(key)) {
                _duplicates.Add(key);
            }
            else {
                dictionary[key] = new HttpControllerDescriptor(_configuration, t.Name, t);
            }
        }

        // Remove any duplicates from the dictionary, because these create ambiguous matches. 
        // For example, "Foo.V1.ProductsController" and "Bar.V1.ProductsController" both map to "v1.products".
        foreach (string s in _duplicates) {
            dictionary.Remove(s);
        }

        return dictionary;
    }

    private static T GetRouteVariable<T>(IHttpRouteData routeData, string name) {
        object result = null;

        if (routeData.Values.TryGetValue(name, out result))
            return (T) result;

        return default(T);
    }
}

问题出在“SelectController”方法中。 在执行“string versionName = GetRouteVariable(routeData, VERSION_KEY);”时,我总是在 versionName 中得到“null” (例如,当我调用“http://localhost:27039/api/v1/products/1”时)。

我应该使用 URL 中请求的版本...

我的 Global.asax.cs :

namespace WebApiApplication {
public class WebApiApplication : System.Web.HttpApplication {
    protected void Application_Start() {
        GlobalConfiguration.Configure(WebApiConfig.Register);
    }
}}

【问题讨论】:

  • 我知道这只是一个测试,但我建议您只创建 1 个控制器并将其发布到 'api/v1' 上。然后,如果您有新版本,您可以在版本控制中创建一个新分支并将其发布到“api/v2”上。假设您需要修复第 1 版中的错误。根据您的策略,您还必须发布第 2 版,因为它们是代表同一应用的 2 个版本的 1 个应用。
  • 两个版本之间的唯一区别是 Product UserReference 在 V2 中变成了字符串

标签: c# asp.net asp.net-web-api asp.net-web-api2 asp.net-web-api-routing


【解决方案1】:

这是一个使用版本约束的更好的解决方案。

public class ApiVersionConstraint : IHttpRouteConstraint
    {
        public string AllowedVersion { get; private set; }

        public ApiVersionConstraint(string allowedVersion)
        {
            AllowedVersion = allowedVersion.ToLowerInvariant();
        }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
        {
            object value;
            if (values.TryGetValue(parameterName, out value) && value != null)
            {
                return AllowedVersion.Equals(value.ToString().ToLowerInvariant());
            }
            return false;
        }
    }

这是 V1 和 V2 的 RoutePrefixAttribute

public class ApiVersion1RoutePrefixAttribute : RoutePrefixAttribute
    {
        private const string RouteBase = "api/{apiVersion:apiVersionConstraint(v1)}";
        private const string PrefixRouteBase = RouteBase + "/";

        public ApiVersion1RoutePrefixAttribute(string routePrefix)
            : base(string.IsNullOrWhiteSpace(routePrefix) ? RouteBase : PrefixRouteBase + routePrefix)
        {
        }
    }


 public class ApiVersion2RoutePrefixAttribute : RoutePrefixAttribute
        {
            private const string RouteBase = "api/{apiVersion:apiVersionConstraint(v2)}";
            private const string PrefixRouteBase = RouteBase + "/";

            public ApiVersion1RoutePrefixAttribute(string routePrefix)
                : base(string.IsNullOrWhiteSpace(routePrefix) ? RouteBase : PrefixRouteBase + routePrefix)
            {
            }
        }

这里是 NamespaceControllerSelector。

public class NamespaceHttpControllerSelector : IHttpControllerSelector
    {
        private readonly HttpConfiguration _configuration;
        private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;

        public NamespaceHttpControllerSelector(HttpConfiguration config)
        {
            _configuration = config;
            _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
        }

        public HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            var routeData = request.GetRouteData();
            if (routeData == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var controllerName = GetControllerName(routeData);
            if (controllerName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var namespaceName = GetVersion(routeData);
            if (namespaceName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, controllerName);

            HttpControllerDescriptor controllerDescriptor;
            if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor))
            {
                return controllerDescriptor;
            }

            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return _controllers.Value;
        }

        private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
        {
            var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);

            var assembliesResolver = _configuration.Services.GetAssembliesResolver();
            var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();

            var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);

            foreach (var controllerType in controllerTypes)
            {
                var segments = controllerType.Namespace.Split(Type.Delimiter);

                var controllerName = controllerType.Name.Remove(controllerType.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);

                var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", segments[segments.Length - 1], controllerName);

                if (!dictionary.Keys.Contains(controllerKey))
                {
                    dictionary[controllerKey] = new HttpControllerDescriptor(_configuration, controllerType.Name, controllerType);
                }
            }

            return dictionary;
        }

        private string GetControllerName(IHttpRouteData routeData)
        {
            var subroute = routeData.GetSubRoutes().FirstOrDefault();
            if (subroute == null) return null;

            var dataTokenValue = subroute.Route.DataTokens.First().Value;
            if (dataTokenValue == null) return null;

            var controllerName = ((HttpActionDescriptor[])dataTokenValue).First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty);
            return controllerName;
        }

        private string GetVersion(IHttpRouteData routeData)
        {
            var subRouteData = routeData.GetSubRoutes().FirstOrDefault();

            if (subRouteData == null) return null;


            // Modified to allow for . versioning, eg v1.1 etc

            var apiVersion = GetRouteVariable<string>(subRouteData, "apiVersion");

            var apiVersionNamespace = apiVersion.Replace('.', '_');

            return apiVersionNamespace;
        }

        private T GetRouteVariable<T>(IHttpRouteData routeData, string name)
        {
            object result;
            if (routeData.Values.TryGetValue(name, out result))
            {
                return (T)result;
            }
            return default(T);
        }
    }

这里是 HttpConfiguration :-

public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {


            var constraintsResolver = new DefaultInlineConstraintResolver();

            constraintsResolver.ConstraintMap.Add("apiVersionConstraint", typeof(ApiVersionConstraint));

            config.MapHttpAttributeRoutes(constraintsResolver);

            config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(config));



        }
    }

最后,这是你的控制器:-

[ApiVersion1RoutePrefix("products")]
public class ProductsController : ApiController {
    Product[] products = new Product[] {
        new Product { UserReference = "a", Name = "product1" },
        new Product { UserReference = "b", Name = "product2" }
    };

    [HttpGet]
    [Route("")]
    public IEnumerable<Product> GetAllProducts() {
        return products;
    }

    [HttpGet]
    [Route("{userReference}")]
    public IHttpActionResult GetProducts(string userReference) {
        var res = products.Where(t => t.UserReference == userReference);

        if (res == null)
            return NotFound();

        return Ok(res);
    }
}

当您想使用 v2 时,只需使用

[ApiVersion2RoutePrefix("products")]

【讨论】:

    【解决方案2】:

    很少有人修改 Mike Wasson 的原始来源。我喜欢ASP.NET Web API 2 book by Jamie Kurtz, Brian Wortman 中使用的那个。

    由于它有太多的移动部件,我创建了a sample project at GitHub。我个人在几个项目中使用了类似的代码。

    删除 RouteConfig 文件中的自定义路由。

    config.Routes.MapHttpRoute(
       name: "DefaultApi",
       routeTemplate: "api/{version}/{controller}",
       defaults: new { version = "v2" }
    );
    
    config.Routes.MapHttpRoute(
       name: "DefaultApiWithId",
       routeTemplate: "api/{version}/{controller}/{id}",
       defaults: new { id = RouteParameter.Optional }
    );
    

    然后,你添加ApiVersionConstraint

    public class ApiVersionConstraint : IHttpRouteConstraint
    {
        public ApiVersionConstraint(string allowedVersion)
        {
            AllowedVersion = allowedVersion.ToLowerInvariant();
        }
    
        public string AllowedVersion { get; private set; }
    
        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
            IDictionary<string, object> values, HttpRouteDirection routeDirection)
        {
            object value;
            if (values.TryGetValue(parameterName, out value) && value != null)
            {
                return AllowedVersion.Equals(value.ToString().ToLowerInvariant());
            }
            return false;
        }
    }
    

    然后,将 RoutePrefix 放在控制器上,就完成了。

    [RoutePrefix("api/{apiVersion:apiVersionConstraint(v1)}/values")]
    public class ValuesController : ApiController
    {
        // GET api/v1/values
        [Route("")]
        public IEnumerable<string> Get()
        {
            return new string[] { "v1-value1", "v1-value2" };
        }
    
        // GET api/v1/values/5
        [Route("{id}")]
        public string Get(int id)
        {
            return "v1-value-" + id;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2020-08-03
      • 2014-09-27
      • 2020-10-19
      • 2013-09-02
      • 2016-12-23
      • 1970-01-01
      • 2016-11-13
      • 2023-03-03
      • 2021-01-02
      相关资源
      最近更新 更多