Mohamad MohamadPoor
Mohamad MohamadPoor

Reputation: 1340

Asp.net WebForm Web API version handling using namespace and persist old API

I have a asp.net WebForm project that contain Web Api v2. I didn't consider api versioning and add my whole api with simple route below:

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

But, now, I need to create versioning because I have lots of change and don't want to disappoint my older client which use old api. So I create a new folder called APIv2 and create my controller there (which has the same name as the old api). The problem is how could I route something like this :

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

I've read lots of post but none of them work for me ! Also I create NamespaceHttpControllerSelector, But it doesn't work too.

please give me some example of how to handle this.

P.S : when I create simple routing such as the old one for new api, it says duplicate controller found ! (Although I am using different namespace)

Edit (Add My Complete Code) :

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}");

And the 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;
    }

}

Upvotes: 0

Views: 1526

Answers (2)

Sergei Rogovtcev
Sergei Rogovtcev

Reputation: 5832

It is not enough to just add NamespaceHttpControllerSelector, you have to provide data for it.

This line

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

tells that it is expecting a list of namespaces to look for controller in a data token called "Namespaces". But the thing is, there's no easy way to set DataTokens in WebAPI (they are originally from MVC), so we'll have to change the code.

Replace

//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));

with

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);

And change your routing as follows:

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"}
);

Upvotes: 1

Arkadiusz K
Arkadiusz K

Reputation: 1827

You can use Web Api attribute routing.

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

Also remember to enable attribute routing in your Web Api configuration

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

More information about attribute routing you can find here : http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2


EDIT

If as you wrote in comment, you have many method in controller and you don't want to add attribute to every method you can use RoutePrefix attribute :

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

Also I don't have WebApiConfig file, where should I create it ?

It is the same place where you have that method invocation:

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

So, in this place you must add following method invocation to enable attribute routing :

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

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

Upvotes: 1

Related Questions