From 7b9396adb9fd100d730da897de97b1c5d981e078 Mon Sep 17 00:00:00 2001 From: gregmac Date: Mon, 6 Feb 2012 01:32:35 -0500 Subject: [PATCH 1/6] Fix for new issues in https://github.com/mccalltd/AttributeRouting/issues/16: request.Form fallback is done with delegate to avoid triggering exception on property access --- .../Extensions/HttpRequestBaseExtensions.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/AttributeRouting/Extensions/HttpRequestBaseExtensions.cs b/src/AttributeRouting/Extensions/HttpRequestBaseExtensions.cs index 81b6c26..8a66858 100644 --- a/src/AttributeRouting/Extensions/HttpRequestBaseExtensions.cs +++ b/src/AttributeRouting/Extensions/HttpRequestBaseExtensions.cs @@ -1,6 +1,7 @@ using System.Collections.Specialized; using System.Reflection; using System.Web; +using System; namespace AttributeRouting.Extensions { @@ -10,22 +11,22 @@ internal static class HttpRequestBaseExtensions public static string GetFormValue(this HttpRequestBase request, string key) { - return request.GetUnvalidatedCollectionOr("Form", request.Form)[key]; + return request.GetUnvalidatedCollectionOr("Form", () => request.Form)[key]; } public static string GetQueryStringValue(this HttpRequestBase request, string key) { - return request.GetUnvalidatedCollectionOr("QueryString", request.QueryString)[key]; + return request.GetUnvalidatedCollectionOr("QueryString", () => request.QueryString)[key]; } /// /// Loads the Form or QueryString collection from the unvalidated object in System.Web.Webpages, /// if that assembly is available. /// - private static NameValueCollection GetUnvalidatedCollectionOr(this HttpRequestBase request, string unvalidatedObjectPropertyName, NameValueCollection defaultCollection) + private static NameValueCollection GetUnvalidatedCollectionOr(this HttpRequestBase request, string unvalidatedObjectPropertyName, Func defaultCollection) { if (_isSystemWebWebPagesUnavailable) - return defaultCollection; + return defaultCollection.Invoke(); try { @@ -42,7 +43,7 @@ private static NameValueCollection GetUnvalidatedCollectionOr(this HttpRequestBa { _isSystemWebWebPagesUnavailable = true; - return defaultCollection; + return defaultCollection.Invoke(); } } } From a116578060033f3f4baca9db271fd73143ffc89d Mon Sep 17 00:00:00 2001 From: Greg MacLellan Date: Mon, 23 Jul 2012 16:04:31 -0400 Subject: [PATCH 2/6] Support for versioning (#91) Initial support for versioning, as described on https://github.com/mccalltd/AttributeRouting/issues/91 --- .../Api/Controllers/VersionedController.cs | 34 ++ .../AttributeRouting.Tests.Web.csproj | 1 + src/AttributeRouting.Tests.Web/Global.asax.cs | 16 +- .../HttpRouteAttribute.cs | 24 ++ .../RouteAttribute.cs | 25 ++ .../Framework/AttributeRoute.cs | 22 ++ src/AttributeRouting/AttributeRouting.csproj | 3 + .../AttributeRoutingConfigurationBase.cs | 5 + .../Framework/IAttributeRoute.cs | 11 + .../Framework/RouteBuilder.cs | 61 ++- .../Framework/RouteReflector.cs | 366 +++++++++--------- .../Framework/RouteSpecification.cs | 6 + src/AttributeRouting/IRouteAttribute.cs | 15 + .../RouteVersionedAttribute.cs | 49 +++ src/AttributeRouting/SemanticVersion.cs | 306 +++++++++++++++ 15 files changed, 748 insertions(+), 196 deletions(-) create mode 100644 src/AttributeRouting.Tests.Web/Areas/Api/Controllers/VersionedController.cs create mode 100644 src/AttributeRouting/RouteVersionedAttribute.cs create mode 100644 src/AttributeRouting/SemanticVersion.cs diff --git a/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/VersionedController.cs b/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/VersionedController.cs new file mode 100644 index 0000000..e6c98ca --- /dev/null +++ b/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/VersionedController.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using AttributeRouting.Web.Mvc; + +namespace AttributeRouting.Tests.Web.Areas.Api.Controllers +{ + [RouteVersioned(MinVer = "1.0")] + public class VersionedController : BaseApiController + { + // + // GET: /Versioned/ + [GET("Versioned", MinVer = "0.0")] + public ActionResult Index() + { + return new ContentResult() { Content = "This is something" }; + } + + [GET("Versioned/{id}", MinVer="1.1")] + public ActionResult Show(int id) + { + return new ContentResult() { Content = "This is something" }; + } + + [GET("Versioned/SingleVersion", MinVer="1.0", MaxVer="1.0")] + public ActionResult New() + { + return new ContentResult() { Content = "This is something" }; + } + + } +} diff --git a/src/AttributeRouting.Tests.Web/AttributeRouting.Tests.Web.csproj b/src/AttributeRouting.Tests.Web/AttributeRouting.Tests.Web.csproj index a6681f0..4d68ed2 100644 --- a/src/AttributeRouting.Tests.Web/AttributeRouting.Tests.Web.csproj +++ b/src/AttributeRouting.Tests.Web/AttributeRouting.Tests.Web.csproj @@ -127,6 +127,7 @@ + Global.asax diff --git a/src/AttributeRouting.Tests.Web/Global.asax.cs b/src/AttributeRouting.Tests.Web/Global.asax.cs index c162a3b..5d855a2 100644 --- a/src/AttributeRouting.Tests.Web/Global.asax.cs +++ b/src/AttributeRouting.Tests.Web/Global.asax.cs @@ -66,7 +66,14 @@ public static void RegisterRoutes(RouteCollection routes) config.ScanAssemblyOf(); config.AddDefaultRouteConstraint(@"[Ii]d$", new RegexRouteConstraint(@"^\d+$")); config.UseRouteHandler(() => new HttpCultureAwareRoutingHandler()); - config.AddTranslationProvider(translationProvider); + config.AddTranslationProvider(translationProvider); + config.ApiVersions = new List() + { + new SemanticVersion("0.9"), + new SemanticVersion("1.0"), + new SemanticVersion("1.1"), + new SemanticVersion("1.2") + }; config.UseLowercaseRoutes = true; config.InheritActionsFromBaseController = true; }); @@ -76,6 +83,13 @@ public static void RegisterRoutes(RouteCollection routes) config.ScanAssemblyOf(); config.AddDefaultRouteConstraint(@"[Ii]d$", new RegexRouteConstraint(@"^\d+$")); config.AddTranslationProvider(translationProvider); + config.ApiVersions = new List() + { + new SemanticVersion("0.9"), + new SemanticVersion("1.0"), + new SemanticVersion("1.1"), + new SemanticVersion("1.2") + }; config.UseRouteHandler(() => new CultureAwareRouteHandler()); config.UseLowercaseRoutes = true; config.InheritActionsFromBaseController = true; diff --git a/src/AttributeRouting.Web.Http/HttpRouteAttribute.cs b/src/AttributeRouting.Web.Http/HttpRouteAttribute.cs index 794ced8..1315dfa 100644 --- a/src/AttributeRouting.Web.Http/HttpRouteAttribute.cs +++ b/src/AttributeRouting.Web.Http/HttpRouteAttribute.cs @@ -72,5 +72,29 @@ public bool AppendTrailingSlash } public bool? AppendTrailingSlashFlag { get; private set; } + + public bool IsVersioned { get; set; } + + public SemanticVersion MinVersion { get; set; } + + public SemanticVersion MaxVersion { get; set; } + + /// + /// Shortcut to set with a string + /// + public string MinVer + { + get { return MinVersion.ToString(); } + set { MinVersion = SemanticVersion.Parse(value, allowNull: true); } + } + + /// + /// Shortcut to set with a string + /// + public string MaxVer + { + get { return MaxVersion.ToString(); } + set { MaxVersion = SemanticVersion.Parse(value, allowNull: true); } + } } } \ No newline at end of file diff --git a/src/AttributeRouting.Web.Mvc/RouteAttribute.cs b/src/AttributeRouting.Web.Mvc/RouteAttribute.cs index 232d34b..792c0be 100644 --- a/src/AttributeRouting.Web.Mvc/RouteAttribute.cs +++ b/src/AttributeRouting.Web.Mvc/RouteAttribute.cs @@ -83,5 +83,30 @@ public override bool IsValidForRequest(ControllerContext controllerContext, Meth var method = controllerContext.HttpContext.Request.GetHttpMethodOverride(); return HttpMethods.Any(m => m.ValueEquals(method)); } + + public bool IsVersioned { get; set; } + + public SemanticVersion MinVersion { get; set; } + + public SemanticVersion MaxVersion { get; set; } + + /// + /// Shortcut to set with a string + /// + public string MinVer + { + get { return MinVersion.ToString(); } + set { MinVersion = SemanticVersion.Parse(value, allowNull: true); } + } + + /// + /// Shortcut to set with a string + /// + public string MaxVer + { + get { return MaxVersion.ToString(); } + set { MaxVersion = SemanticVersion.Parse(value, allowNull: true); } + } + } } \ No newline at end of file diff --git a/src/AttributeRouting.Web/Framework/AttributeRoute.cs b/src/AttributeRouting.Web/Framework/AttributeRoute.cs index c743a2f..142cce5 100644 --- a/src/AttributeRouting.Web/Framework/AttributeRoute.cs +++ b/src/AttributeRouting.Web/Framework/AttributeRoute.cs @@ -60,6 +60,28 @@ IDictionary IAttributeRoute.Defaults public IAttributeRoute DefaultRouteContainer { get; set; } + public SemanticVersion MinVersion { get; set; } + + public SemanticVersion MaxVersion { get; set; } + + /// + /// Shortcut to set with a string + /// + public string MinVer + { + get { return MinVersion.ToString(); } + set { MinVersion = SemanticVersion.Parse(value, allowNull: true); } + } + + /// + /// Shortcut to set with a string + /// + public string MaxVer + { + get { return MaxVersion.ToString(); } + set { MaxVersion = SemanticVersion.Parse(value, allowNull: true); } + } + public override RouteData GetRouteData(HttpContextBase httpContext) { // Optimize matching by comparing the static left part of the route url with the requested path. diff --git a/src/AttributeRouting/AttributeRouting.csproj b/src/AttributeRouting/AttributeRouting.csproj index 3089b7d..170ce9e 100644 --- a/src/AttributeRouting/AttributeRouting.csproj +++ b/src/AttributeRouting/AttributeRouting.csproj @@ -47,6 +47,7 @@ + @@ -102,6 +103,8 @@ + + diff --git a/src/AttributeRouting/AttributeRoutingConfigurationBase.cs b/src/AttributeRouting/AttributeRoutingConfigurationBase.cs index f3a78f9..cea4d25 100644 --- a/src/AttributeRouting/AttributeRoutingConfigurationBase.cs +++ b/src/AttributeRouting/AttributeRoutingConfigurationBase.cs @@ -76,6 +76,11 @@ protected AttributeRoutingConfigurationBase() /// public List TranslationProviders { get; set; } + /// + /// List of supported API versions + /// + public List ApiVersions { get; set; } + /// /// When true, the generated routes will produce lowercase URLs. /// The default is false. diff --git a/src/AttributeRouting/Framework/IAttributeRoute.cs b/src/AttributeRouting/Framework/IAttributeRoute.cs index 78cef57..d72e9de 100644 --- a/src/AttributeRouting/Framework/IAttributeRoute.cs +++ b/src/AttributeRouting/Framework/IAttributeRoute.cs @@ -73,6 +73,17 @@ public interface IAttributeRoute /// IEnumerable Translations { get; set; } + /// + /// Minimum supported version, or null for no minimum + /// + SemanticVersion MinVersion { get; set; } + + /// + /// Maximum supported version, or null for no maximum + /// + SemanticVersion MaxVersion { get; set; } + + /// /// Default route container back-reference /// diff --git a/src/AttributeRouting/Framework/RouteBuilder.cs b/src/AttributeRouting/Framework/RouteBuilder.cs index 555deaa..c7fc439 100644 --- a/src/AttributeRouting/Framework/RouteBuilder.cs +++ b/src/AttributeRouting/Framework/RouteBuilder.cs @@ -38,23 +38,47 @@ public IEnumerable BuildAllRoutes() foreach (var routeSpec in routeSpecs) { - foreach (var route in Build(routeSpec)) + foreach (var version in GenerateRouteVersions(routeSpec)) { - route.MappedSubdomains = mappedSubdomains; - yield return route; + foreach (var route in Build(routeSpec, version)) + { + route.MappedSubdomains = mappedSubdomains; + yield return route; + } } } } - private IEnumerable Build(RouteSpecification routeSpec) + /// + /// Looks at _configuration.ApiVersions to see what versions are supported, and generates a list + /// of versions for each between min and max. + /// If no versions are defined in configuration, always returns one null version. + /// + /// + /// + /// + private IEnumerable GenerateRouteVersions(RouteSpecification routeSpec) + { + if (!routeSpec.IsVersioned || (_configuration.ApiVersions == null) && (_configuration.ApiVersions.Count == 0)) + { + return new List() {null}; + } + + return (from version in _configuration.ApiVersions + where (routeSpec.MinVersion == null || version >= routeSpec.MinVersion) + && (routeSpec.MaxVersion == null || version <= routeSpec.MaxVersion) + select version); + } + + private IEnumerable Build(RouteSpecification routeSpec, SemanticVersion version) { - var route = _routeFactory.CreateAttributeRoute(CreateRouteUrl(routeSpec), + var route = _routeFactory.CreateAttributeRoute(CreateRouteUrl(routeSpec, version), CreateRouteDefaults(routeSpec), - CreateRouteConstraints(routeSpec), + CreateRouteConstraints(routeSpec, version), CreateRouteDataTokens(routeSpec)); route.RouteName = CreateRouteName(routeSpec); - route.Translations = CreateRouteTranslations(routeSpec); + route.Translations = CreateRouteTranslations(routeSpec, version); route.Subdomain = routeSpec.Subdomain; route.UseLowercaseRoute = routeSpec.UseLowercaseRoute; route.PreserveCaseForUrlParameters = routeSpec.PreserveCaseForUrlParameters; @@ -89,17 +113,18 @@ private string CreateRouteName(RouteSpecification routeSpec) return null; } - - private string CreateRouteUrl(RouteSpecification routeSpec) + + private string CreateRouteUrl(RouteSpecification routeSpec, SemanticVersion version) { return CreateRouteUrl(routeSpec.RouteUrl, routeSpec.RoutePrefixUrl, routeSpec.AreaUrl, + version == null ? null : version.ToString(), routeSpec.IsAbsoluteUrl, routeSpec.UseLowercaseRoute); } - private string CreateRouteUrl(string routeUrl, string routePrefix, string areaUrl, bool isAbsoluteUrl, bool? useLowercaseRoute) + private string CreateRouteUrl(string routeUrl, string routePrefix, string areaUrl, string verisonPrefix, bool isAbsoluteUrl, bool? useLowercaseRoute) { var detokenizedUrl = DetokenizeUrl(routeUrl); @@ -130,6 +155,13 @@ private string CreateRouteUrl(string routeUrl, string routePrefix, string areaUr urlBuilder.Insert(0, delimitedRoutePrefix); } + if (verisonPrefix.HasValue()) + { + var delimitedVerisonPrefix = verisonPrefix + "/"; + if (!delimitedRouteUrl.StartsWith(delimitedVerisonPrefix)) + urlBuilder.Insert(0, delimitedVerisonPrefix); + } + if (areaUrl.HasValue()) { var delimitedAreaUrl = areaUrl + "/"; @@ -212,7 +244,7 @@ private IDictionary CreateRouteDefaults(RouteSpecification route return defaults; } - private IDictionary CreateRouteConstraints(RouteSpecification routeSpec) + private IDictionary CreateRouteConstraints(RouteSpecification routeSpec, SemanticVersion version) { var constraints = new Dictionary(); @@ -305,7 +337,7 @@ private IDictionary CreateRouteConstraints(RouteSpecification ro constraints.Add(constraintAttribute.Key, constraintAttribute.Constraint); } - var detokenizedUrl = DetokenizeUrl(CreateRouteUrl(routeSpec)); + var detokenizedUrl = DetokenizeUrl(CreateRouteUrl(routeSpec, version)); var urlParameterNames = GetUrlParameterContents(detokenizedUrl).ToList(); // Globally configured constraints @@ -355,7 +387,7 @@ private static string DetokenizeUrl(string url) return Regex.Replace(url, String.Join("|", patterns), ""); } - private IEnumerable CreateRouteTranslations(RouteSpecification routeSpec) + private IEnumerable CreateRouteTranslations(RouteSpecification routeSpec, SemanticVersion version) { // If no translation provider, then get out of here. if (!_configuration.TranslationProviders.Any()) @@ -386,10 +418,11 @@ private IEnumerable CreateRouteTranslations(RouteSpecification _routeFactory.CreateAttributeRoute(CreateRouteUrl(translatedRouteUrl ?? routeSpec.RouteUrl, translatedRoutePrefix ?? routeSpec.RoutePrefixUrl, translatedAreaUrl ?? routeSpec.AreaUrl, + version == null ? null : version.ToString(), routeSpec.IsAbsoluteUrl, routeSpec.UseLowercaseRoute), CreateRouteDefaults(routeSpec), - CreateRouteConstraints(routeSpec), + CreateRouteConstraints(routeSpec, version), CreateRouteDataTokens(routeSpec)); translatedRoute.CultureName = cultureName; diff --git a/src/AttributeRouting/Framework/RouteReflector.cs b/src/AttributeRouting/Framework/RouteReflector.cs index ae35ed8..c6be3bd 100644 --- a/src/AttributeRouting/Framework/RouteReflector.cs +++ b/src/AttributeRouting/Framework/RouteReflector.cs @@ -1,182 +1,186 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using AttributeRouting.Helpers; - -namespace AttributeRouting.Framework -{ - /// - /// A reflector that inspects the assemblies provided in configuration to find AttributeRouting attributes. - /// - public class RouteReflector - { - private readonly AttributeRoutingConfigurationBase _configuration; - - public RouteReflector(AttributeRoutingConfigurationBase configuration) - { - if (configuration == null) throw new ArgumentNullException("configuration"); - - _configuration = configuration; - } - - public IEnumerable GenerateRouteSpecifications() - { - var controllerRouteSpecs = GenerateRouteSpecifications(_configuration.PromotedControllerTypes, _configuration.InheritActionsFromBaseController); - foreach (var spec in controllerRouteSpecs) - yield return spec; - - if (!_configuration.Assemblies.Any()) - yield break; - - var scannedControllerTypes = _configuration.Assemblies.SelectMany(a => a.GetControllerTypes(_configuration.FrameworkControllerType)).ToList(); - var remainingControllerTypes = scannedControllerTypes.Except(_configuration.PromotedControllerTypes); - var remainingRouteSpecs = GenerateRouteSpecifications(remainingControllerTypes, _configuration.InheritActionsFromBaseController); - - foreach (var spec in remainingRouteSpecs) - yield return spec; - } - - private IEnumerable GenerateRouteSpecifications(IEnumerable controllerTypes, bool inheritActionsFromBaseController) - { - var controllerCount = 0; - - return (from controllerType in controllerTypes - let controllerIndex = controllerCount++ - let convention = controllerType.GetCustomAttribute(false) - let routeAreaAttribute = controllerType.GetCustomAttribute(true) - let routePrefixAttribute = controllerType.GetCustomAttribute(true) - from actionMethod in controllerType.GetActionMethods(inheritActionsFromBaseController) - from routeAttribute in GetRouteAttributes(actionMethod, convention) - // precedence is within a controller - orderby controllerIndex, routeAttribute.Precedence - let routeName = routeAttribute.RouteName - let subdomain = GetAreaSubdomain(routeAreaAttribute) - let isAsyncController = controllerType.IsAsyncController() - select new RouteSpecification - { - AreaName = routeAreaAttribute.SafeGet(a => a.AreaName), - AreaUrl = GetAreaUrl(routeAreaAttribute, subdomain), - AreaUrlTranslationKey = routeAreaAttribute.SafeGet(a => a.TranslationKey), - Subdomain = subdomain, - RoutePrefixUrl = GetRoutePrefix(routePrefixAttribute, actionMethod, convention), - RoutePrefixUrlTranslationKey = routePrefixAttribute.SafeGet(a => a.TranslationKey), - ControllerType = controllerType, - ControllerName = controllerType.GetControllerName(), - ActionName = GetActionName(actionMethod, isAsyncController), - RouteUrl = routeAttribute.RouteUrl, - RouteUrlTranslationKey = routeAttribute.TranslationKey, - HttpMethods = routeAttribute.HttpMethods, - DefaultAttributes = GetDefaultAttributes(actionMethod, routeName, convention), - ConstraintAttributes = GetConstraintAttributes(actionMethod, routeName, convention), - RouteName = routeName, - IsAbsoluteUrl = routeAttribute.IsAbsoluteUrl, - UseLowercaseRoute = routeAttribute.UseLowercaseRouteFlag, - PreserveCaseForUrlParameters = routeAttribute.PreserveCaseForUrlParametersFlag, - AppendTrailingSlash = routeAttribute.AppendTrailingSlashFlag - }).ToList(); - } - - private static string GetActionName(MethodInfo actionMethod, bool isAsyncController) - { - string actionName = actionMethod.Name; - if (isAsyncController && actionName.EndsWith("Async")) - actionName = actionName.Substring(0, actionName.Length - 5); - return actionName; - } - - private static IEnumerable GetRouteAttributes(MethodInfo actionMethod, RouteConventionAttributeBase convention) - { - var attributes = new List(); - - // Add convention-based attributes - if (convention != null) - attributes.AddRange(convention.GetRouteAttributes(actionMethod)); - - // Add explicitly-defined attributes - attributes.AddRange(actionMethod.GetCustomAttributes(false)); - - return attributes.OrderBy(a => a.Order); - } - - private static string GetAreaUrl(RouteAreaAttribute routeAreaAttribute, string subdomain) - { - if (routeAreaAttribute == null) - return null; - - // If a subdomain is specified for the area either in the RouteAreaAttribute - // or via configuration, then assume the area url is blank; eg: admin.badass.com. - // However, our fearless coder can decide to explicitly specify an area url if desired; - // eg: internal.badass.com/admin. - if (subdomain.HasValue() && routeAreaAttribute.AreaUrl.HasNoValue()) - return null; - - return routeAreaAttribute.AreaUrl ?? routeAreaAttribute.AreaName; - } - - private string GetAreaSubdomain(RouteAreaAttribute routeAreaAttribute) - { - if (routeAreaAttribute == null) - return null; - - // Check for a subdomain override specified via configuration object. - var subdomainOverride = (from o in _configuration.AreaSubdomainOverrides - where o.Key == routeAreaAttribute.AreaName - select o.Value).FirstOrDefault(); - - if (subdomainOverride != null) - return subdomainOverride; - - return routeAreaAttribute.Subdomain; - } - - private static string GetRoutePrefix(RoutePrefixAttribute routePrefixAttribute, MethodInfo actionMethod, RouteConventionAttributeBase convention) - { - // Return an explicitly defined route prefix, if defined - if (routePrefixAttribute != null) - return routePrefixAttribute.Url; - - // Otherwise, if this is a convention-based controller, get the convention-based prefix - if (convention != null) - return convention.GetDefaultRoutePrefix(actionMethod); - - return null; - } - - private static ICollection GetDefaultAttributes(MethodInfo actionMethod, string routeName, RouteConventionAttributeBase convention) - { - var defaultAttributes = new List(); - - // Yield explicitly defined default attributes first - defaultAttributes.AddRange( - from defaultAttribute in actionMethod.GetCustomAttributes(false) - where !defaultAttribute.ForRouteNamed.HasValue() || - defaultAttribute.ForRouteNamed == routeName - select defaultAttribute); - - // Yield convention-based defaults next - if (convention != null) - defaultAttributes.AddRange(convention.GetRouteDefaultAttributes(actionMethod)); - - return defaultAttributes.ToList(); - } - - private static ICollection GetConstraintAttributes(MethodInfo actionMethod, string routeName, RouteConventionAttributeBase convention) - { - var constraintAttributes = new List(); - - // Yield explicitly defined constraint attributes first - constraintAttributes.AddRange( - from constraintAttribute in actionMethod.GetCustomAttributes(false) - where !constraintAttribute.ForRouteNamed.HasValue() || - constraintAttribute.ForRouteNamed == routeName - select constraintAttribute); - - // Yield convention-based constraints next - if (convention != null) - constraintAttributes.AddRange(convention.GetRouteConstraintAttributes(actionMethod)); - - return constraintAttributes.ToList(); - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AttributeRouting.Helpers; + +namespace AttributeRouting.Framework +{ + /// + /// A reflector that inspects the assemblies provided in configuration to find AttributeRouting attributes. + /// + public class RouteReflector + { + private readonly AttributeRoutingConfigurationBase _configuration; + + public RouteReflector(AttributeRoutingConfigurationBase configuration) + { + if (configuration == null) throw new ArgumentNullException("configuration"); + + _configuration = configuration; + } + + public IEnumerable GenerateRouteSpecifications() + { + var controllerRouteSpecs = GenerateRouteSpecifications(_configuration.PromotedControllerTypes, _configuration.InheritActionsFromBaseController); + foreach (var spec in controllerRouteSpecs) + yield return spec; + + if (!_configuration.Assemblies.Any()) + yield break; + + var scannedControllerTypes = _configuration.Assemblies.SelectMany(a => a.GetControllerTypes(_configuration.FrameworkControllerType)).ToList(); + var remainingControllerTypes = scannedControllerTypes.Except(_configuration.PromotedControllerTypes); + var remainingRouteSpecs = GenerateRouteSpecifications(remainingControllerTypes, _configuration.InheritActionsFromBaseController); + + foreach (var spec in remainingRouteSpecs) + yield return spec; + } + + private IEnumerable GenerateRouteSpecifications(IEnumerable controllerTypes, bool inheritActionsFromBaseController) + { + var controllerCount = 0; + + return (from controllerType in controllerTypes + let controllerIndex = controllerCount++ + let convention = controllerType.GetCustomAttribute(false) + let routeAreaAttribute = controllerType.GetCustomAttribute(true) + let routePrefixAttribute = controllerType.GetCustomAttribute(true) + let routeVersionedAttribute = controllerType.GetCustomAttribute(true) + from actionMethod in controllerType.GetActionMethods(inheritActionsFromBaseController) + from routeAttribute in GetRouteAttributes(actionMethod, convention) + // precedence is within a controller + orderby controllerIndex, routeAttribute.Precedence + let routeName = routeAttribute.RouteName + let subdomain = GetAreaSubdomain(routeAreaAttribute) + let isAsyncController = controllerType.IsAsyncController() + select new RouteSpecification + { + AreaName = routeAreaAttribute.SafeGet(a => a.AreaName), + AreaUrl = GetAreaUrl(routeAreaAttribute, subdomain), + AreaUrlTranslationKey = routeAreaAttribute.SafeGet(a => a.TranslationKey), + Subdomain = subdomain, + RoutePrefixUrl = GetRoutePrefix(routePrefixAttribute, actionMethod, convention), + RoutePrefixUrlTranslationKey = routePrefixAttribute.SafeGet(a => a.TranslationKey), + ControllerType = controllerType, + ControllerName = controllerType.GetControllerName(), + ActionName = GetActionName(actionMethod, isAsyncController), + RouteUrl = routeAttribute.RouteUrl, + RouteUrlTranslationKey = routeAttribute.TranslationKey, + HttpMethods = routeAttribute.HttpMethods, + DefaultAttributes = GetDefaultAttributes(actionMethod, routeName, convention), + ConstraintAttributes = GetConstraintAttributes(actionMethod, routeName, convention), + RouteName = routeName, + IsAbsoluteUrl = routeAttribute.IsAbsoluteUrl, + UseLowercaseRoute = routeAttribute.UseLowercaseRouteFlag, + PreserveCaseForUrlParameters = routeAttribute.PreserveCaseForUrlParametersFlag, + AppendTrailingSlash = routeAttribute.AppendTrailingSlashFlag, + IsVersioned = routeVersionedAttribute != null && routeVersionedAttribute.IsVersioned, + MinVersion = routeAttribute.MinVersion ?? (routeVersionedAttribute != null ? routeVersionedAttribute.MinVersion : null), + MaxVersion = routeAttribute.MaxVersion ?? (routeVersionedAttribute != null ? routeVersionedAttribute.MaxVersion : null) + }).ToList(); + } + + private static string GetActionName(MethodInfo actionMethod, bool isAsyncController) + { + string actionName = actionMethod.Name; + if (isAsyncController && actionName.EndsWith("Async")) + actionName = actionName.Substring(0, actionName.Length - 5); + return actionName; + } + + private static IEnumerable GetRouteAttributes(MethodInfo actionMethod, RouteConventionAttributeBase convention) + { + var attributes = new List(); + + // Add convention-based attributes + if (convention != null) + attributes.AddRange(convention.GetRouteAttributes(actionMethod)); + + // Add explicitly-defined attributes + attributes.AddRange(actionMethod.GetCustomAttributes(false)); + + return attributes.OrderBy(a => a.Order); + } + + private static string GetAreaUrl(RouteAreaAttribute routeAreaAttribute, string subdomain) + { + if (routeAreaAttribute == null) + return null; + + // If a subdomain is specified for the area either in the RouteAreaAttribute + // or via configuration, then assume the area url is blank; eg: admin.badass.com. + // However, our fearless coder can decide to explicitly specify an area url if desired; + // eg: internal.badass.com/admin. + if (subdomain.HasValue() && routeAreaAttribute.AreaUrl.HasNoValue()) + return null; + + return routeAreaAttribute.AreaUrl ?? routeAreaAttribute.AreaName; + } + + private string GetAreaSubdomain(RouteAreaAttribute routeAreaAttribute) + { + if (routeAreaAttribute == null) + return null; + + // Check for a subdomain override specified via configuration object. + var subdomainOverride = (from o in _configuration.AreaSubdomainOverrides + where o.Key == routeAreaAttribute.AreaName + select o.Value).FirstOrDefault(); + + if (subdomainOverride != null) + return subdomainOverride; + + return routeAreaAttribute.Subdomain; + } + + private static string GetRoutePrefix(RoutePrefixAttribute routePrefixAttribute, MethodInfo actionMethod, RouteConventionAttributeBase convention) + { + // Return an explicitly defined route prefix, if defined + if (routePrefixAttribute != null) + return routePrefixAttribute.Url; + + // Otherwise, if this is a convention-based controller, get the convention-based prefix + if (convention != null) + return convention.GetDefaultRoutePrefix(actionMethod); + + return null; + } + + private static ICollection GetDefaultAttributes(MethodInfo actionMethod, string routeName, RouteConventionAttributeBase convention) + { + var defaultAttributes = new List(); + + // Yield explicitly defined default attributes first + defaultAttributes.AddRange( + from defaultAttribute in actionMethod.GetCustomAttributes(false) + where !defaultAttribute.ForRouteNamed.HasValue() || + defaultAttribute.ForRouteNamed == routeName + select defaultAttribute); + + // Yield convention-based defaults next + if (convention != null) + defaultAttributes.AddRange(convention.GetRouteDefaultAttributes(actionMethod)); + + return defaultAttributes.ToList(); + } + + private static ICollection GetConstraintAttributes(MethodInfo actionMethod, string routeName, RouteConventionAttributeBase convention) + { + var constraintAttributes = new List(); + + // Yield explicitly defined constraint attributes first + constraintAttributes.AddRange( + from constraintAttribute in actionMethod.GetCustomAttributes(false) + where !constraintAttribute.ForRouteNamed.HasValue() || + constraintAttribute.ForRouteNamed == routeName + select constraintAttribute); + + // Yield convention-based constraints next + if (convention != null) + constraintAttributes.AddRange(convention.GetRouteConstraintAttributes(actionMethod)); + + return constraintAttributes.ToList(); + } + } } \ No newline at end of file diff --git a/src/AttributeRouting/Framework/RouteSpecification.cs b/src/AttributeRouting/Framework/RouteSpecification.cs index dccea14..a0eeff6 100644 --- a/src/AttributeRouting/Framework/RouteSpecification.cs +++ b/src/AttributeRouting/Framework/RouteSpecification.cs @@ -48,5 +48,11 @@ public RouteSpecification() public bool? PreserveCaseForUrlParameters { get; set; } public bool? AppendTrailingSlash { get; set; } + + public bool IsVersioned { get; set; } + + public SemanticVersion MinVersion { get; set; } + + public SemanticVersion MaxVersion { get; set; } } } \ No newline at end of file diff --git a/src/AttributeRouting/IRouteAttribute.cs b/src/AttributeRouting/IRouteAttribute.cs index 665358e..a587c9d 100644 --- a/src/AttributeRouting/IRouteAttribute.cs +++ b/src/AttributeRouting/IRouteAttribute.cs @@ -74,5 +74,20 @@ public interface IRouteAttribute /// Gets the tri-state value for AppendTrailingSlash. /// bool? AppendTrailingSlashFlag { get; } + + /// + /// Indicates if this route is versioned. Is set by defining the [RouteVersioned] attribute on the class + /// + bool IsVersioned { get; set; } + + /// + /// The minimum version required. + /// + SemanticVersion MinVersion { get; set; } + + /// + /// The maximum version required. + /// + SemanticVersion MaxVersion { get; set; } } } \ No newline at end of file diff --git a/src/AttributeRouting/RouteVersionedAttribute.cs b/src/AttributeRouting/RouteVersionedAttribute.cs new file mode 100644 index 0000000..2a57bcb --- /dev/null +++ b/src/AttributeRouting/RouteVersionedAttribute.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace AttributeRouting +{ + // indicates that this route is versioned + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class RouteVersionedAttribute : Attribute + { + public RouteVersionedAttribute(bool isVersioned = true) + { + IsVersioned = isVersioned; + } + + public bool IsVersioned { get; private set; } + + /// + /// Minimum verison supported. Optional, can be overridden by individual route attributes. + /// + public SemanticVersion MinVersion { get; set; } + + /// + /// Maximum verison supported. Optional, can be overridden by individual route attributes. + /// + public SemanticVersion MaxVersion { get; set; } + + + /// + /// Shortcut to set with a string + /// + public string MinVer + { + get { return MinVersion.ToString(); } + set { MinVersion = SemanticVersion.Parse(value, allowNull:true); } + } + + /// + /// Shortcut to set with a string + /// + public string MaxVer + { + get { return MaxVersion.ToString(); } + set { MaxVersion = SemanticVersion.Parse(value, allowNull:true); } + } + + } +} diff --git a/src/AttributeRouting/SemanticVersion.cs b/src/AttributeRouting/SemanticVersion.cs new file mode 100644 index 0000000..bf8c37e --- /dev/null +++ b/src/AttributeRouting/SemanticVersion.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Web.Util; + + +namespace AttributeRouting +{ + /// + /// A hybrid implementation of SemVer that supports semantic versioning as described at http://semver.org while not strictly enforcing it to + /// allow older 4-digit versioning schemes to continue working. + /// From NuGet project. + /// + [Serializable] + [TypeConverter(typeof(SemanticVersionTypeConverter))] + public sealed class SemanticVersion : IComparable, IComparable, IEquatable + { + private const RegexOptions _flags = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; + private static readonly Regex _semanticVersionRegex = new Regex(@"^(?\d+(\s*\.\s*\d+){0,3})(?-[a-z][0-9a-z-]*)?$", _flags); + private static readonly Regex _strictSemanticVersionRegex = new Regex(@"^(?\d+(\.\d+){2})(?-[a-z][0-9a-z-]*)?$", _flags); + private readonly string _originalString; + + public SemanticVersion(string version) + : this(Parse(version)) + { + // The constructor normalizes the version string so that it we do not need to normalize it every time we need to operate on it. + // The original string represents the original form in which the version is represented to be used when printing. + _originalString = version; + } + + public SemanticVersion(int major, int minor, int build, int revision) + : this(new Version(major, minor, build, revision)) + { + } + + public SemanticVersion(int major, int minor, int build, string specialVersion) + : this(new Version(major, minor, build), specialVersion) + { + } + + public SemanticVersion(Version version) + : this(version, String.Empty) + { + } + + public SemanticVersion(Version version, string specialVersion) + : this(version, specialVersion, null) + { + } + + private SemanticVersion(Version version, string specialVersion, string originalString) + { + if (version == null) + { + throw new ArgumentNullException("version"); + } + Version = NormalizeVersionValue(version); + SpecialVersion = specialVersion ?? String.Empty; + _originalString = String.IsNullOrEmpty(originalString) ? version.ToString() + (!String.IsNullOrEmpty(specialVersion) ? '-' + specialVersion : null) : originalString; + } + + internal SemanticVersion(SemanticVersion semVer) + { + _originalString = semVer.ToString(); + Version = semVer.Version; + SpecialVersion = semVer.SpecialVersion; + } + + /// + /// Gets the normalized version portion. + /// + public Version Version + { + get; + private set; + } + + /// + /// Gets the optional special version. + /// + public string SpecialVersion + { + get; + private set; + } + + /// + /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. + /// + /// The version string + /// If true, null or empty version string returns null. Otherwise, throws an exception + public static SemanticVersion Parse(string version, bool allowNull = false) + { + if (String.IsNullOrEmpty(version)) + { + if (allowNull) return null; + throw new ArgumentException("Argument cannot be null or empty", "version"); + } + + SemanticVersion semVer; + if (!TryParse(version, out semVer)) + { + throw new ArgumentException(string.Format("\"{0}\" is invalid as a version string", version), "version"); + } + return semVer; + } + + /// + /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. + /// + public static bool TryParse(string version, out SemanticVersion value) + { + return TryParseInternal(version, _semanticVersionRegex, out value); + } + + /// + /// Parses a version string using strict semantic versioning rules that allows exactly 3 components and an optional special version. + /// + public static bool TryParseStrict(string version, out SemanticVersion value) + { + return TryParseInternal(version, _strictSemanticVersionRegex, out value); + } + + private static bool TryParseInternal(string version, Regex regex, out SemanticVersion semVer) + { + semVer = null; + if (String.IsNullOrEmpty(version)) + { + return false; + } + + var match = regex.Match(version.Trim()); + Version versionValue; + if (!match.Success || !Version.TryParse(match.Groups["Version"].Value, out versionValue)) + { + return false; + } + + semVer = new SemanticVersion(NormalizeVersionValue(versionValue), match.Groups["Release"].Value.TrimStart('-'), version.Replace(" ", "")); + return true; + } + + /// + /// Attempts to parse the version token as a SemanticVersion. + /// Note the AttributeRouting version of this differs from the original NuGet SemanticVersion implementation: + /// if version is empty or null, a null version is returned. If version is invalid, an exception is thrown. + /// + /// An instance of SemanticVersion if it parses correctly, null otherwise. + public static SemanticVersion ParseOptionalVersion(string version) + { + SemanticVersion semVer; + TryParse(version, out semVer); + return semVer; + } + + private static Version NormalizeVersionValue(Version version) + { + return new Version(version.Major, + version.Minor, + Math.Max(version.Build, 0), + Math.Max(version.Revision, 0)); + } + + public int CompareTo(object obj) + { + if (Object.ReferenceEquals(obj, null)) + { + return 1; + } + SemanticVersion other = obj as SemanticVersion; + if (other == null) + { + throw new ArgumentException("Type of obj must be SemanticVersion", "obj"); + } + return CompareTo(other); + } + + public int CompareTo(SemanticVersion other) + { + if (Object.ReferenceEquals(other, null)) + { + return 1; + } + + int result = Version.CompareTo(other.Version); + + if (result != 0) + { + return result; + } + + bool empty = String.IsNullOrEmpty(SpecialVersion); + bool otherEmpty = String.IsNullOrEmpty(other.SpecialVersion); + if (empty && otherEmpty) + { + return 0; + } + else if (empty) + { + return 1; + } + else if (otherEmpty) + { + return -1; + } + return StringComparer.OrdinalIgnoreCase.Compare(SpecialVersion, other.SpecialVersion); + } + + public static bool operator ==(SemanticVersion version1, SemanticVersion version2) + { + if (Object.ReferenceEquals(version1, null)) + { + return Object.ReferenceEquals(version2, null); + } + return version1.Equals(version2); + } + + public static bool operator !=(SemanticVersion version1, SemanticVersion version2) + { + return !(version1 == version2); + } + + public static bool operator <(SemanticVersion version1, SemanticVersion version2) + { + if (version1 == null) + { + throw new ArgumentNullException("version1"); + } + return version1.CompareTo(version2) < 0; + } + + public static bool operator <=(SemanticVersion version1, SemanticVersion version2) + { + return (version1 == version2) || (version1 < version2); + } + + public static bool operator >(SemanticVersion version1, SemanticVersion version2) + { + if (version1 == null) + { + throw new ArgumentNullException("version1"); + } + return version2 < version1; + } + + public static bool operator >=(SemanticVersion version1, SemanticVersion version2) + { + return (version1 == version2) || (version1 > version2); + } + + public override string ToString() + { + return _originalString; + } + + public bool Equals(SemanticVersion other) + { + return !Object.ReferenceEquals(null, other) && + Version.Equals(other.Version) && + SpecialVersion.Equals(other.SpecialVersion, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object obj) + { + SemanticVersion semVer = obj as SemanticVersion; + return !Object.ReferenceEquals(null, semVer) && Equals(semVer); + } + + public override int GetHashCode() + { + //var hashCodeCombiner = new HashCodeCombiner(); + //hashCodeCombiner.AddObject(Version); + //hashCodeCombiner.AddObject(SpecialVersion); + //return hashCodeCombiner.CombinedHash; + // Note, taken from source to HashCodeCombiner + var combinedHash = 5381; // Start with a seed (obtained from String.GetHashCode implementation) + if (Version != null) combinedHash = ((combinedHash << 5) + combinedHash) ^ Version.GetHashCode(); + if (SpecialVersion != null) combinedHash = ((combinedHash << 5) + combinedHash) ^ SpecialVersion.GetHashCode(); + return combinedHash.GetHashCode(); + } + } + + public class SemanticVersionTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + var stringValue = value as string; + SemanticVersion semVer; + if (stringValue != null && SemanticVersion.TryParse(stringValue, out semVer)) + { + return semVer; + } + return null; + } + } + +} \ No newline at end of file From 9dcb0bd19b6135bf314474cc4cd0303cc5496679 Mon Sep 17 00:00:00 2001 From: Greg MacLellan Date: Mon, 23 Jul 2012 16:19:28 -0400 Subject: [PATCH 3/6] Untabify file --- .../Framework/RouteReflector.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/AttributeRouting/Framework/RouteReflector.cs b/src/AttributeRouting/Framework/RouteReflector.cs index c6be3bd..cc99bca 100644 --- a/src/AttributeRouting/Framework/RouteReflector.cs +++ b/src/AttributeRouting/Framework/RouteReflector.cs @@ -53,7 +53,7 @@ from routeAttribute in GetRouteAttributes(actionMethod, convention) orderby controllerIndex, routeAttribute.Precedence let routeName = routeAttribute.RouteName let subdomain = GetAreaSubdomain(routeAreaAttribute) - let isAsyncController = controllerType.IsAsyncController() + let isAsyncController = controllerType.IsAsyncController() select new RouteSpecification { AreaName = routeAreaAttribute.SafeGet(a => a.AreaName), @@ -64,7 +64,7 @@ from routeAttribute in GetRouteAttributes(actionMethod, convention) RoutePrefixUrlTranslationKey = routePrefixAttribute.SafeGet(a => a.TranslationKey), ControllerType = controllerType, ControllerName = controllerType.GetControllerName(), - ActionName = GetActionName(actionMethod, isAsyncController), + ActionName = GetActionName(actionMethod, isAsyncController), RouteUrl = routeAttribute.RouteUrl, RouteUrlTranslationKey = routeAttribute.TranslationKey, HttpMethods = routeAttribute.HttpMethods, @@ -81,13 +81,13 @@ from routeAttribute in GetRouteAttributes(actionMethod, convention) }).ToList(); } - private static string GetActionName(MethodInfo actionMethod, bool isAsyncController) - { - string actionName = actionMethod.Name; - if (isAsyncController && actionName.EndsWith("Async")) - actionName = actionName.Substring(0, actionName.Length - 5); - return actionName; - } + private static string GetActionName(MethodInfo actionMethod, bool isAsyncController) + { + string actionName = actionMethod.Name; + if (isAsyncController && actionName.EndsWith("Async")) + actionName = actionName.Substring(0, actionName.Length - 5); + return actionName; + } private static IEnumerable GetRouteAttributes(MethodInfo actionMethod, RouteConventionAttributeBase convention) { From 19d18259bd694d174f3d1eeac3925c526be95e9a Mon Sep 17 00:00:00 2001 From: Greg MacLellan Date: Mon, 23 Jul 2012 16:31:30 -0400 Subject: [PATCH 4/6] Add simpler versioning configuration API New AddVersions() method to make configuration of versioning (#91) simpler. --- src/AttributeRouting.Tests.Web/Global.asax.cs | 16 ++------------ .../AttributeRoutingConfigurationBase.cs | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/AttributeRouting.Tests.Web/Global.asax.cs b/src/AttributeRouting.Tests.Web/Global.asax.cs index 5d855a2..bbd6591 100644 --- a/src/AttributeRouting.Tests.Web/Global.asax.cs +++ b/src/AttributeRouting.Tests.Web/Global.asax.cs @@ -67,13 +67,7 @@ public static void RegisterRoutes(RouteCollection routes) config.AddDefaultRouteConstraint(@"[Ii]d$", new RegexRouteConstraint(@"^\d+$")); config.UseRouteHandler(() => new HttpCultureAwareRoutingHandler()); config.AddTranslationProvider(translationProvider); - config.ApiVersions = new List() - { - new SemanticVersion("0.9"), - new SemanticVersion("1.0"), - new SemanticVersion("1.1"), - new SemanticVersion("1.2") - }; + config.AddVersions("0.9","1.0","1.1","1.2"); config.UseLowercaseRoutes = true; config.InheritActionsFromBaseController = true; }); @@ -83,13 +77,7 @@ public static void RegisterRoutes(RouteCollection routes) config.ScanAssemblyOf(); config.AddDefaultRouteConstraint(@"[Ii]d$", new RegexRouteConstraint(@"^\d+$")); config.AddTranslationProvider(translationProvider); - config.ApiVersions = new List() - { - new SemanticVersion("0.9"), - new SemanticVersion("1.0"), - new SemanticVersion("1.1"), - new SemanticVersion("1.2") - }; + config.AddVersions("0.9", "1.0", "1.1", "1.2"); config.UseRouteHandler(() => new CultureAwareRouteHandler()); config.UseLowercaseRoutes = true; config.InheritActionsFromBaseController = true; diff --git a/src/AttributeRouting/AttributeRoutingConfigurationBase.cs b/src/AttributeRouting/AttributeRoutingConfigurationBase.cs index cea4d25..908ec32 100644 --- a/src/AttributeRouting/AttributeRoutingConfigurationBase.cs +++ b/src/AttributeRouting/AttributeRoutingConfigurationBase.cs @@ -29,6 +29,8 @@ protected AttributeRoutingConfigurationBase() InlineRouteConstraints = new Dictionary(); TranslationProviders = new List(); + + ApiVersions = new List(); AreaSubdomainOverrides = new Dictionary(); DefaultSubdomain = "www"; @@ -218,6 +220,25 @@ from cultureName in provider.CultureNames select cultureName).Distinct().ToList(); } + /// + /// Adds to the list of supported . + /// + public void AddVersions(params string[] versions) + { + foreach (var version in versions) + { + ApiVersions.Add(SemanticVersion.Parse(version)); + } + } + + /// + /// Adds to the list of supported . + /// + public void AddVersions(params SemanticVersion[] versions) + { + ApiVersions.AddRange(versions); + } + protected void RegisterDefaultInlineRouteConstraints(Assembly assembly) { // Register default inline route constraints From 9d2600e24c2d9dd51687777982052eef80b5b802 Mon Sep 17 00:00:00 2001 From: Greg MacLellan Date: Mon, 23 Jul 2012 16:42:43 -0400 Subject: [PATCH 5/6] Support versioning in SelfHost Web API Missed a spot implementing #91 --- .../Framework/HttpAttributeRoute.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/AttributeRouting.Web.Http.SelfHost/Framework/HttpAttributeRoute.cs b/src/AttributeRouting.Web.Http.SelfHost/Framework/HttpAttributeRoute.cs index 3f52bca..eac012a 100644 --- a/src/AttributeRouting.Web.Http.SelfHost/Framework/HttpAttributeRoute.cs +++ b/src/AttributeRouting.Web.Http.SelfHost/Framework/HttpAttributeRoute.cs @@ -46,6 +46,10 @@ public string Url public bool? AppendTrailingSlash { get; set; } + public SemanticVersion MinVersion { get; set; } + + public SemanticVersion MaxVersion { get; set; } + IDictionary IAttributeRoute.DataTokens { get { return DataTokens; } From 43bcd9891a580ea29e3fc1b485c21a358512c2dd Mon Sep 17 00:00:00 2001 From: gregmac Date: Wed, 16 Jan 2013 12:25:34 -0500 Subject: [PATCH 6/6] Add HttpGet to VersionedController (since it's using non-conventional names), and add example of different controller actions for different versions of the same URL --- .../Api/Controllers/VersionedController.cs | 33 +++++++++++++++---- .../Models/VersionedModels.cs | 20 +++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 src/AttributeRouting.Tests.Web/Models/VersionedModels.cs diff --git a/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/VersionedController.cs b/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/VersionedController.cs index ffe70b6..4425ecc 100644 --- a/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/VersionedController.cs +++ b/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/VersionedController.cs @@ -1,4 +1,7 @@ -using AttributeRouting.Web.Http; +using System; +using System.Web.Http; +using AttributeRouting.Tests.Web.Models; +using AttributeRouting.Web.Http; namespace AttributeRouting.Tests.Web.Areas.Api.Controllers { @@ -7,23 +10,41 @@ public class VersionedController : BaseApiController { // // GET: /Versioned/ - [GET("Versioned", MinVer = "0.0")] - public string Index() + [HttpGet, GET("Versioned", MaxVer = "1.1")] + public VersionedModel_old Index_old() { - return "This is /versioned"; + return new VersionedModel_old() {Text = "This is /versioned (up to 1.1)", GeneratedTime = DateTime.Now}; } - [GET("Versioned/{id}", MinVer="1.1")] + [HttpGet, GET("Versioned", MinVer = "1.2")] + public VersionedModel Index() + { + return new VersionedModel() + { + Title = "This is /versioned", + Body = "This model is added in 1.2, and returns title/body isntead of just text", + GeneratedTime = DateTime.Now + }; + } + + [HttpGet, GET("Versioned/{id}", MinVer = "1.1")] public string Show(int id) { return string.Format("This is /versioned/id with id = {0}", id); } - [GET("Versioned/SingleVersion", MinVer="1.0", MaxVer="1.0")] + [HttpGet, GET("Versioned/SingleVersion", MinVer = "1.0", MaxVer = "1.0")] public string New() { return "This should only work with version 1.0"; } + [HttpGet, GET("Versioned/BeforeV1", MinVer = "0.0")] + public string BeforeV1() + { + return "This existed in versions even prior to 1.0 (overrides class-level version)"; + } + + } } diff --git a/src/AttributeRouting.Tests.Web/Models/VersionedModels.cs b/src/AttributeRouting.Tests.Web/Models/VersionedModels.cs new file mode 100644 index 0000000..b2717de --- /dev/null +++ b/src/AttributeRouting.Tests.Web/Models/VersionedModels.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace AttributeRouting.Tests.Web.Models +{ + public class VersionedModel_old + { + public string Text { get; set; } + public DateTime GeneratedTime { get; set; } + } + + public class VersionedModel + { + public string Title { get; set; } + public string Body { get; set; } + public DateTime GeneratedTime { get; set; } + } +} \ No newline at end of file