【问题标题】:Asp.net WebForm Web API version handling using namespace and persist old API使用命名空间处理 Asp.net WebForm Web API 版本并保留旧 API
【发布时间】:2015-09-25 19:43:54
【问题描述】:

我有一个包含 Web Api v2 的 asp.net WebForm 项目。我没有考虑 api 版本控制,而是用下面的简单路线添加了我的整个 api:

RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}"
);

但是,现在,我需要创建版本控制,因为我有很多更改并且不想让使用旧 API 的老客户失望。所以我创建了一个名为 APIv2 的新文件夹,并在那里创建了我的控制器(与旧 api 同名)。问题是我怎么能路由这样的东西:

MyWebsite/API/Item => For ItemController OF OLD API
MyWebsite/APIv2/Item => For ItemController Of New API (Version 2)

我已经阅读了很多帖子,但没有一个对我有用!我也创建了NamespaceHttpControllerSelector,但它也不起作用。

请举例说明如何处理。

P.S:当我为新 api 创建简单的路由(例如旧路由)时,它说找到了重复的控制器! (虽然我使用不同的命名空间)

编辑(添加我的完整代码):

GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(GlobalConfiguration.Configuration));

        RouteTable.Routes.MapHttpRoute(
        name: "VersionedApi",
        routeTemplate: "api/v2/{controller}");

        RouteTable.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}");

还有 NamespaceHttpControllerSelector:

public class NamespaceHttpControllerSelector : DefaultHttpControllerSelector
{
    private const string ControllerKey = "controller";
    private readonly HttpConfiguration _configuration;
    private readonly Lazy<IEnumerable<Type>> _duplicateControllerTypes;

    public NamespaceHttpControllerSelector(HttpConfiguration configuration) : base(configuration)
    {
        _configuration = configuration;
        _duplicateControllerTypes = new Lazy<IEnumerable<Type>>(GetDuplicateControllerTypes);
    }

    public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {
        var routeData = request.GetRouteData();
        if (routeData == null || routeData.Route == null || routeData.Route.DataTokens == null || routeData.Route.DataTokens["Namespaces"] == null) 
            return base.SelectController(request);

        // Look up controller in route data
        object controllerName;
        routeData.Values.TryGetValue(ControllerKey, out controllerName);
        var controllerNameAsString = controllerName as string;
        if (controllerNameAsString == null) 
            return base.SelectController(request);

        //get the currently cached default controllers - this will not contain duplicate controllers found so if
        // this controller is found in the underlying cache we don't need to do anything
        var map = base.GetControllerMapping();
        if (map.ContainsKey(controllerNameAsString)) 
            return base.SelectController(request);

        //the cache does not contain this controller because it's most likely a duplicate, 
        // so we need to sort this out ourselves and we can only do that if the namespace token
        // is formatted correctly.
        var namespaces = routeData.Route.DataTokens["Namespaces"] as IEnumerable<string>;
        if (namespaces == null)
            return base.SelectController(request);

        //see if this is in our cache
        var found = _duplicateControllerTypes.Value
            .Where(x => string.Equals(x.Name, controllerNameAsString + ControllerSuffix, StringComparison.OrdinalIgnoreCase))
            .FirstOrDefault(x => namespaces.Contains(x.Namespace));

        if (found == null)
            return base.SelectController(request);

        return new HttpControllerDescriptor(_configuration, controllerNameAsString, found);
    }

    private IEnumerable<Type> GetDuplicateControllerTypes()
    {
        var assembliesResolver = _configuration.Services.GetAssembliesResolver();
        var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
        var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);

        //we have all controller types, so just store the ones with duplicate class names - we don't
        // want to cache too much and the underlying selector caches everything else

        var duplicates = controllerTypes.GroupBy(x => x.Name)
            .Where(x => x.Count() > 1)
            .SelectMany(x => x)
            .ToArray();

        return duplicates;
    }

}

【问题讨论】:

  • 你读过this article吗?你提到NamespaceHttpControllerSelector,是从那里来的吗?请发布有关 WebAPI 配置的完整代码。
  • @SergRogovtsev 我读了那篇文章和另一篇,都不起作用!我还编辑帖子并发布我的完整代码(到目前为止的最新版本)。
  • 按照文章做到底有什么不行的?
  • @SergRogovtsev 我收到此错误:对“项目”的请求已找到以下匹配的控制器:Pooshanet.API.v2.ItemController Pooshanet.API.ItemController,可能是因为它自己的命名空间?
  • 您确定您指的是我上面链接的文章,而不是您发布的代码吗?它们是不同的。

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


【解决方案1】:

仅仅添加NamespaceHttpControllerSelector是不够的,还得提供数据。

这一行

var namespaces = routeData.Route.DataTokens["Namespaces"] as IEnumerable<string>;

告诉它期望命名空间列表在名为“命名空间”的数据令牌中查找控制器。但问题是,在 WebAPI 中设置 DataTokens 没有简单的方法(它们最初来自 MVC),所以我们必须更改代码。

替换

//the cache does not contain this controller because it's most likely a duplicate, 
// so we need to sort this out ourselves and we can only do that if the namespace token
// is formatted correctly.
var namespaces = routeData.Route.DataTokens["Namespaces"] as IEnumerable<string>;
if (namespaces == null)
  return base.SelectController(request);

//see if this is in our cache
var found = _duplicateControllerTypes.Value
  .Where(x => string.Equals(x.Name, controllerNameAsString + ControllerSuffix, StringComparison.OrdinalIgnoreCase))
  .FirstOrDefault(x => namespaces.Contains(x.Namespace));

var @namespace = routeData.Values["namespace"] as string;
if (@namespace == null)
  return base.SelectController(request);

//see if this is in our cache
var found = _duplicateControllerTypes.Value
  .Where(x => string.Equals(x.Name, controllerNameAsString + ControllerSuffix, StringComparison.OrdinalIgnoreCase))
  .FirstOrDefault(x => x.Namespace == @namespace);

并按如下方式更改您的路由:

RouteTable.Routes.MapHttpRoute(
  name: "VersionedApi",
  routeTemplate: "api/v2/{controller}",
  defaults: new {@namespace = "your namespace for v2 controllers"}
);

RouteTable.Routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}",
  defaults: new {@namespace = "your namespace for v1 controllers"}
);

【讨论】:

  • 感谢您的回答,我再次导致重复错误并说:“项目”的请求已找到以下匹配的控制器:Pooshanet.API.v2.ItemController Pooshanet.API.ItemController,可能是因为它自己的命名空间? (另外我将您的代码更改为 new string[]{"my namespace name"},您的会导致错误)
  • 哦,我明白了,您的控制器选择器依赖于 DataTokens。更新了答案。
  • 再次出现同样的错误! (命名空间是一个关键字,用代码中的命名空间更改它)
  • 再次更新了答案中的代码(抱歉,在文本编辑器中很难编程)。
【解决方案2】:

可以使用Web Api属性路由。

public ItemControllerV2 : ApiController
{
    [Route("v2/item/{id:int}")]
    public Item Get(int id)
    {
        ....
    }
}

还要记得在你的 Web Api 配置中启用属性路由

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();
        //rest of your Web Api configuration
    }
 }

更多关于属性路由的信息你可以在这里找到:http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2


编辑

如果正如您在评论中所写,您在控制器中有很多方法,并且您不想为每个可以使用RoutePrefix 属性的方法添加属性:

[RoutePrefix("v2/item")]
public ItemControllerV2 : ApiController
{
}

另外我没有 WebApiConfig 文件,我应该在哪里创建它?

与您调用该方法的位置相同:

RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}"
);

所以,在这个地方你必须添加以下方法调用来启用属性路由:

GlobalConfiguration.Configure(config => 
{
    config.MapHttpAttributeRoutes();

    RouteTable.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}"
    );
});

【讨论】:

  • 感谢您的回答,但是有很多方法,我应该为所有方法添加吗?有没有办法把所有的东西分开?我也没有 WebApiConfig 文件,我应该在哪里创建它?
  • 再次感谢,其实我没有 WebApiConfig 文件,我在 Global.asax 中添加了路由部分(Application_Start 方法)!
  • 我明白了。特别是直接在Application_Start 方法中进行所有配置并没有错。但是,如果您的项目增长,它可以受益于将各种元素配置分离为单独的类。您可以在Application_Start 方法中进一步使用这些类,但由于它变得更易于维护。
  • Application_Start 中没有“配置”可以使用!我添加了这个 GlobalConfiguration.Configuration.MapHttpAttributeRoutes();在 Session_Start 中(在 Application_Start 中会导致错误),但没有任何变化!
  • 你是对的 我认为你使用默认模板来配置 WebApi 是我的错误。我更正了启用属性路由的调用。无论如何,你应该把它放在Application_Start。你在Application_Start使用时遇到了什么错误?
猜你喜欢
  • 1970-01-01
  • 2016-08-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-03
  • 2020-09-20
  • 2014-09-27
  • 1970-01-01
相关资源
最近更新 更多