2016-03-29 9 views
7

Nel mio progetto ho implementato i vincoli di routing personalizzati per consentire il versioning dell'API tramite una variabile di intestazione personalizzata (api-version), simile a this sample project on Codeplex, anche se ho modificato il vincolo per consentire per una convention major.minor.Ottenere le API della Guida Web per lavorare con i vincoli di routing personalizzati

Questo comporta la creazione di due controllori separati i cui percorsi sono differenziati tramite un attributo FullVersionedRoute:

Sample1Controller.cs

/// <summary> 
/// v1.0 Controller 
/// </summary> 
public class Sample1Controller : ApiController 
{ 
    [FullVersionedRoute("api/test", "1.0")] 
    public IEnumerable<string> Get() 
    { 
     return new[] { "This is version 1.0 test!" }; 
    } 
} 

Sample2Controller.cs

/// <summary> 
/// v2.0 Controller 
/// </summary> 
public class Sample2Controller : ApiController 
{ 
    [FullVersionedRoute("api/test", "2.0")] 
    public IEnumerable<string> Get() 
    { 
     return new[] { "This is version 2.0 test!" }; 
    } 
} 

FullVersionedRout e.cs

using System.Collections.Generic; 
    using System.Web.Http.Routing; 

namespace HelperClasses.Versioning 
{ 
    /// <summary> 
    /// Provides an attribute route that's restricted to a specific version of the api. 
    /// </summary> 
    internal class FullVersionedRoute : RouteFactoryAttribute 
    { 
     public FullVersionedRoute(string template, string allowedVersion) : base(template) 
     { 
      AllowedVersion = allowedVersion; 
     } 

     public string AllowedVersion 
     { 
      get; 
      private set; 
     } 

     public override IDictionary<string, object> Constraints 
     { 
      get 
      { 
       var constraints = new HttpRouteValueDictionary(); 
       constraints.Add("version", new FullVersionConstraint(AllowedVersion)); 
       return constraints; 
      } 
     } 
    } 
} 

FullVersionConstraint.cs

using System.Collections.Generic; 
using System.Linq; 
using System.Net.Http; 
using System.Web.Http.Routing; 

namespace HelperClasses.Versioning 
{ 
    /// <summary> 
    /// A Constraint implementation that matches an HTTP header against an expected version value. 
    /// </summary> 
    internal class FullVersionConstraint : IHttpRouteConstraint 
    { 
     public const string VersionHeaderName = "api-version"; 

     private const string DefaultVersion = "1.0"; 

     public FullVersionConstraint(string allowedVersion) 
     { 
      AllowedVersion = allowedVersion; 
     } 

     public string AllowedVersion 
     { 
      get; 
      private set; 
     } 

     public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) 
     { 
      if (routeDirection == HttpRouteDirection.UriResolution) 
      { 
       var version = GetVersionHeader(request) ?? DefaultVersion; 
       return (version == AllowedVersion); 
      } 

      return false; 
     } 

     private string GetVersionHeader(HttpRequestMessage request) 
     { 
      IEnumerable<string> headerValues; 

      if (request.Headers.TryGetValues(VersionHeaderName, out headerValues)) 
      { 
       // enumerate the list once 
       IEnumerable<string> headers = headerValues.ToList(); 

       // if we find once instance of the target header variable, return it 
       if (headers.Count() == 1) 
       { 
        return headers.First(); 
       } 
      } 

      return null; 
     } 
    } 
} 

Questo funziona bene, ma il auto-generated help files non può distinguere tra le azioni nei due controller come appaiono come lo stesso percorso (se si presta attenzione solo alla rotta dell'URL, cosa che avviene di default). Pertanto, l'azione di Sample2Controller.cs sovrascrive l'azione da Sample1Controller.cs in modo che solo l'API Sample2 venga visualizzata nelle pagine della guida.

Esiste un modo per configurare il pacchetto della pagina della Guida dell'API Web per riconoscere un vincolo personalizzato e riconoscere che esistono due API separate e successivamente visualizzarle come gruppi API separati nelle pagine della Guida?

+0

Ho un problema simile utilizzando solo i percorsi personalizzati. La mia domanda è perché non distribuire su server/approot/versione, sembra un po 'strano avere più versioni annidate nella stessa base di codice di ogni re-deploy è una modifica a tutte le versioni anche se non ci dovrebbe essere nulla di modificato nella logica per la versione originale non può essere al 100% – workabyte

+0

Secondo questo articolo, è perché questa soluzione darebbe l'impressione di avere più risorse distinte (cioè v1/myresource, v2/myresource, v3/myresource). Quando in realtà, c'è una risorsa (ad esempio/myresource), solo diverse versioni di essa (servita in base all'intestazione Accept, che è il suo scopo in base alla SPEC HTTP). È un argomento semantico. http://www.troyhunt.com/2014/02/your-api-versioning-is-wrong-which-is.html – bperniciaro

risposta

1

Ho trovato this article che descrive come ottenere ciò mediante l'implementazione di IApiExplorer.

In breve, ciò che si vorrà fare è aggiungere una nuova classe VersionedApiExplorer attuazione IApiExplorer in questo modo

using System; 
using System.Collections.ObjectModel; 
using System.Collections.Generic; 
using System.Linq; 
using System.Reflection; 
using System.Web.Http; 
using System.Web.Http.Controllers; 
using System.Web.Http.Description; 
using System.Web.Http.Routing; 

namespace HelperClasses.Versioning 
{ 
    public class VersionedApiExplorer<TVersionConstraint> : IApiExplorer 
    { 
     private IApiExplorer _innerApiExplorer; 
     private HttpConfiguration _configuration; 
     private Lazy<Collection<ApiDescription>> _apiDescriptions; 
     private MethodInfo _apiDescriptionPopulator; 

     public VersionedApiExplorer(IApiExplorer apiExplorer, HttpConfiguration configuration) 
     { 
      _innerApiExplorer = apiExplorer; 
      _configuration = configuration; 
      _apiDescriptions = new Lazy<Collection<ApiDescription>>(
       new Func<Collection<ApiDescription>>(Init)); 
     } 

     public Collection<ApiDescription> ApiDescriptions 
     { 
      get { return _apiDescriptions.Value; } 
     } 

     private Collection<ApiDescription> Init() 
     { 
      var descriptions = _innerApiExplorer.ApiDescriptions; 

      var controllerSelector = _configuration.Services.GetHttpControllerSelector(); 
      var controllerMappings = controllerSelector.GetControllerMapping(); 

      var flatRoutes = FlattenRoutes(_configuration.Routes); 
      var result = new Collection<ApiDescription>(); 

      foreach (var description in descriptions) 
      { 
       result.Add(description); 

       if (controllerMappings != null && description.Route.Constraints.Any(c => c.Value is TVersionConstraint)) 
       { 
        var matchingRoutes = flatRoutes.Where(r => r.RouteTemplate == description.Route.RouteTemplate && r != description.Route); 

        foreach (var route in matchingRoutes) 
         GetRouteDescriptions(route, result); 
       } 
      } 
      return result; 
     } 

     private void GetRouteDescriptions(IHttpRoute route, Collection<ApiDescription> apiDescriptions) 
     { 
      var actionDescriptor = route.DataTokens["actions"] as IEnumerable<HttpActionDescriptor>; 

      if (actionDescriptor != null && actionDescriptor.Count() > 0) 
       GetPopulateMethod().Invoke(_innerApiExplorer, new object[] { actionDescriptor.First(), route, route.RouteTemplate, apiDescriptions }); 
     } 

     private MethodInfo GetPopulateMethod() 
     { 
      if (_apiDescriptionPopulator == null) 
       _apiDescriptionPopulator = _innerApiExplorer.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(
        m => m.Name == "PopulateActionDescriptions" && m.GetParameters().Length == 4); 

      return _apiDescriptionPopulator; 
     } 

     public static IEnumerable<IHttpRoute> FlattenRoutes(IEnumerable<IHttpRoute> routes) 
     { 
      var flatRoutes = new List<HttpRoute>(); 

      foreach (var route in routes) 
      { 
       if (route is HttpRoute) 
        yield return route; 

       var subRoutes = route as IReadOnlyCollection<IHttpRoute>; 
       if (subRoutes != null) 
        foreach (IHttpRoute subRoute in FlattenRoutes(subRoutes)) 
         yield return subRoute; 
      } 
     } 
    } 
} 

e quindi aggiungere questo al vostro WebAPIConfig

var apiExplorer = config.Services.GetApiExplorer(); 
config.Services.Replace(typeof(IApiExplorer), new VersionedApiExplorer<FullVersionConstraint>(apiExplorer, config)); 

Si dovrebbe quindi vedere sia le tue API Sample1 e Sample2 nella tua pagina di aiuto dell'API Web.

+0

Eccellente. Questo ha fatto il trucco! – bperniciaro