diff --git a/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/PlainController.cs b/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/PlainController.cs index d3bde32..6974c1f 100644 --- a/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/PlainController.cs +++ b/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/PlainController.cs @@ -7,7 +7,7 @@ namespace AttributeRouting.Tests.Web.Areas.Api.Controllers public class PlainController : BaseApiController { // GET /api/plain - [GET("")] + [GET("", MinVer = "1.0")] public IEnumerable GetAll() { return new [] { "value1", "value2" }; 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..4425ecc --- /dev/null +++ b/src/AttributeRouting.Tests.Web/Areas/Api/Controllers/VersionedController.cs @@ -0,0 +1,50 @@ +using System; +using System.Web.Http; +using AttributeRouting.Tests.Web.Models; +using AttributeRouting.Web.Http; + +namespace AttributeRouting.Tests.Web.Areas.Api.Controllers +{ + [RouteVersioned(MinVer = "1.0")] + public class VersionedController : BaseApiController + { + // + // GET: /Versioned/ + [HttpGet, GET("Versioned", MaxVer = "1.1")] + public VersionedModel_old Index_old() + { + return new VersionedModel_old() {Text = "This is /versioned (up to 1.1)", GeneratedTime = DateTime.Now}; + } + + [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); + } + + [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/AttributeRouting.Tests.Web.csproj b/src/AttributeRouting.Tests.Web/AttributeRouting.Tests.Web.csproj index b36614c..a091294 100644 --- a/src/AttributeRouting.Tests.Web/AttributeRouting.Tests.Web.csproj +++ b/src/AttributeRouting.Tests.Web/AttributeRouting.Tests.Web.csproj @@ -135,6 +135,7 @@ + @@ -191,10 +192,6 @@ - - {C91C065B-A821-4890-9F31-F9E245D804D1} - AttributeRouting.Web - {CCDE9AD7-3822-4B0B-AA19-DF6698A85D3D} AttributeRouting.Web.Http @@ -207,6 +204,10 @@ {A018FEC5-45F8-44FB-BB6C-33697B418434} AttributeRouting.Web.Http.WebHost + + {C91C065B-A821-4890-9F31-F9E245D804D1} + AttributeRouting.Web + {871A79CF-C705-4C6B-8938-F9AA1E02AEA4} AttributeRouting diff --git a/src/AttributeRouting.Tests.Web/Global.asax.cs b/src/AttributeRouting.Tests.Web/Global.asax.cs index 7ca3a41..0fcc734 100644 --- a/src/AttributeRouting.Tests.Web/Global.asax.cs +++ b/src/AttributeRouting.Tests.Web/Global.asax.cs @@ -68,7 +68,8 @@ public static void RegisterRoutes(RouteCollection routes) config.AddRoutesFromAssemblyOf(); config.AddDefaultRouteConstraint(@"[Ii]d$", new RegexRouteConstraint(@"^\d+$")); config.UseRouteHandler(() => new HttpCultureAwareRoutingHandler()); - config.AddTranslationProvider(translationProvider); + config.AddTranslationProvider(translationProvider); + config.AddVersions("0.9","1.0","1.1","1.2"); config.UseLowercaseRoutes = true; config.InheritActionsFromBaseController = true; config.AutoGenerateRouteNames = true; @@ -79,6 +80,7 @@ public static void RegisterRoutes(RouteCollection routes) config.AddRoutesFromAssemblyOf(); config.AddDefaultRouteConstraint(@"[Ii]d$", new RegexRouteConstraint(@"^\d+$")); config.AddTranslationProvider(translationProvider); + 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.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 diff --git a/src/AttributeRouting.Tests.Web/Web.config b/src/AttributeRouting.Tests.Web/Web.config index d729656..032c063 100644 --- a/src/AttributeRouting.Tests.Web/Web.config +++ b/src/AttributeRouting.Tests.Web/Web.config @@ -49,14 +49,14 @@ - + - + diff --git a/src/AttributeRouting.Web.Http/Framework/HttpAttributeRoute.cs b/src/AttributeRouting.Web.Http/Framework/HttpAttributeRoute.cs index f712271..00707e3 100644 --- a/src/AttributeRouting.Web.Http/Framework/HttpAttributeRoute.cs +++ b/src/AttributeRouting.Web.Http/Framework/HttpAttributeRoute.cs @@ -46,7 +46,12 @@ public string Url public bool? PreserveCaseForUrlParameters { get; set; } - public bool? AppendTrailingSlash { get; set; } + public bool? AppendTrailingSlash { get; set; } + + public SemanticVersion MinVersion { get; set; } + + public SemanticVersion MaxVersion { get; set; } + IDictionary IAttributeRoute.DataTokens { @@ -109,6 +114,6 @@ public override IHttpVirtualPathData GetVirtualPath(HttpRequestMessage request, var virtualPath = this.GetFinalVirtualPath(virtualPathData.VirtualPath, _configuration); return new HttpVirtualPathData(virtualPathData.Route, virtualPath); - } + } } } diff --git a/src/AttributeRouting.Web.Http/HttpRouteAttribute.cs b/src/AttributeRouting.Web.Http/HttpRouteAttribute.cs index 6a7053d..02a1909 100644 --- a/src/AttributeRouting.Web.Http/HttpRouteAttribute.cs +++ b/src/AttributeRouting.Web.Http/HttpRouteAttribute.cs @@ -97,5 +97,29 @@ public bool AppendTrailingSlash public bool IgnoreRoutePrefix { get; set; } public bool IgnoreAreaUrl { get; 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/Framework/AttributeRoute.cs b/src/AttributeRouting.Web.Mvc/Framework/AttributeRoute.cs index 1f6cf70..3ece52e 100644 --- a/src/AttributeRouting.Web.Mvc/Framework/AttributeRoute.cs +++ b/src/AttributeRouting.Web.Mvc/Framework/AttributeRoute.cs @@ -40,7 +40,11 @@ public AttributeRoute(string url, public bool? PreserveCaseForUrlParameters { get; set; } - public bool? AppendTrailingSlash { get; set; } + public bool? AppendTrailingSlash { get; set; } + + public SemanticVersion MinVersion { get; set; } + + public SemanticVersion MaxVersion { get; set; } IDictionary IAttributeRoute.DataTokens { diff --git a/src/AttributeRouting.Web.Mvc/RouteAttribute.cs b/src/AttributeRouting.Web.Mvc/RouteAttribute.cs index 7f73314..d938e76 100644 --- a/src/AttributeRouting.Web.Mvc/RouteAttribute.cs +++ b/src/AttributeRouting.Web.Mvc/RouteAttribute.cs @@ -132,5 +132,30 @@ public override bool IsValidForRequest(ControllerContext controllerContext, Meth return HttpMethods.Any(m => m.ValueEquals(httpMethod)); } + + 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/AttributeRouting.csproj b/src/AttributeRouting/AttributeRouting.csproj index 6004846..b44e11f 100644 --- a/src/AttributeRouting/AttributeRouting.csproj +++ b/src/AttributeRouting/AttributeRouting.csproj @@ -53,6 +53,7 @@ + @@ -109,6 +110,8 @@ + + diff --git a/src/AttributeRouting/ConfigurationBase.cs b/src/AttributeRouting/ConfigurationBase.cs index 1a41ca2..841d7cc 100644 --- a/src/AttributeRouting/ConfigurationBase.cs +++ b/src/AttributeRouting/ConfigurationBase.cs @@ -24,7 +24,9 @@ protected ConfigurationBase() Assemblies = new List(); OrderedControllerTypes = new List(); - InheritActionsFromBaseController = false; + InheritActionsFromBaseController = false; + + ApiVersions = new List(); // Constraint setting initialization DefaultRouteConstraints = new Dictionary(); @@ -134,7 +136,14 @@ protected ConfigurationBase() /// Constrains translated routes by the thread's current UI culture. /// The default is false. /// - public bool ConstrainTranslatedRoutesByCurrentUICulture { get; set; } + public bool ConstrainTranslatedRoutesByCurrentUICulture { get; set; } + + + /// + /// List of supported API versions + /// + public List ApiVersions { get; set; } + /// /// Returns a utility for configuring areas when initializing AttributeRouting framework. @@ -278,5 +287,25 @@ where typeof(TRouteConstraint).IsAssignableFrom(t) InlineRouteConstraints.Add(name, inlineConstraintType); } } + + /// + /// 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); + } } } \ No newline at end of file diff --git a/src/AttributeRouting/Framework/IAttributeRoute.cs b/src/AttributeRouting/Framework/IAttributeRoute.cs index 61b469e..c4c13ac 100644 --- a/src/AttributeRouting/Framework/IAttributeRoute.cs +++ b/src/AttributeRouting/Framework/IAttributeRoute.cs @@ -79,6 +79,18 @@ public interface IAttributeRoute /// /// Default route container back-reference, used to organize route translations. + /// 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 /// IAttributeRoute DefaultRouteContainer { get; set; } } diff --git a/src/AttributeRouting/Framework/RouteBuilder.cs b/src/AttributeRouting/Framework/RouteBuilder.cs index 793277b..db56fe7 100644 --- a/src/AttributeRouting/Framework/RouteBuilder.cs +++ b/src/AttributeRouting/Framework/RouteBuilder.cs @@ -43,20 +43,44 @@ where s.Subdomain.HasValue() 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 defaults = CreateRouteDefaults(routeSpec); - var constraints = CreateRouteConstraints(routeSpec); + var constraints = CreateRouteConstraints(routeSpec, version); var dataTokens = CreateRouteDataTokens(routeSpec); - var url = CreateRouteUrl(defaults, routeSpec); + var url = CreateRouteUrl(defaults, routeSpec, version); var routes = _routeFactory.CreateAttributeRoutes(url, defaults, constraints, dataTokens); @@ -69,7 +93,7 @@ private IEnumerable Build(RouteSpecification routeSpec) route.DataTokens.Add("routeName", routeName); } - route.Translations = CreateRouteTranslations(routeSpec); + route.Translations = CreateRouteTranslations(routeSpec, version); route.Subdomain = routeSpec.Subdomain; route.UseLowercaseRoute = routeSpec.UseLowercaseRoute; route.PreserveCaseForUrlParameters = routeSpec.PreserveCaseForUrlParameters; @@ -100,18 +124,19 @@ private string CreateRouteName(RouteSpecification routeSpec) return _configuration.AutoGenerateRouteNames ? _configuration.RouteNameBuilder(routeSpec) : null; } - private string CreateRouteUrl(IDictionary defaults, RouteSpecification routeSpec) + private string CreateRouteUrl(IDictionary defaults, RouteSpecification routeSpec, SemanticVersion version) { return CreateRouteUrl(routeSpec.RouteUrl, routeSpec.RoutePrefixUrl, routeSpec.AreaUrl, + version, defaults, routeSpec); } - private string CreateRouteUrl(string routeUrl, string routePrefix, string areaUrl, IDictionary defaults, RouteSpecification routeSpec) + private string CreateRouteUrl(string routeUrl, string routePrefix, string areaUrl, SemanticVersion version, IDictionary defaults, RouteSpecification routeSpec) { - var tokenizedUrl = BuildTokenizedUrl(routeUrl, routePrefix, areaUrl, routeSpec); + var tokenizedUrl = BuildTokenizedUrl(routeUrl, routePrefix, areaUrl, version, routeSpec); var tokenizedPath = RemoveQueryString(tokenizedUrl); var detokenizedPath = DetokenizeUrl(tokenizedPath); @@ -197,7 +222,7 @@ private IDictionary CreateRouteDefaults(RouteSpecification route return defaults; } - private IDictionary CreateRouteConstraints(RouteSpecification routeSpec) + private IDictionary CreateRouteConstraints(RouteSpecification routeSpec, SemanticVersion version) { var constraints = new Dictionary(); @@ -206,7 +231,7 @@ private IDictionary CreateRouteConstraints(RouteSpecification ro constraints.Add("inboundHttpMethod", _routeConstraintFactory.CreateInboundHttpMethodConstraint(routeSpec.HttpMethods)); // Work from a complete, tokenized url; ie: support constraints in area urls, route prefix urls, and route urls. - var tokenizedUrl = BuildTokenizedUrl(routeSpec.RouteUrl, routeSpec.RoutePrefixUrl, routeSpec.AreaUrl, routeSpec); + var tokenizedUrl = BuildTokenizedUrl(routeSpec.RouteUrl, routeSpec.RoutePrefixUrl, routeSpec.AreaUrl, version, routeSpec); var urlParameters = GetUrlParameterContents(tokenizedUrl).ToList(); // Need to keep track of query params. @@ -323,7 +348,7 @@ private IDictionary CreateRouteConstraints(RouteSpecification ro return constraints; } - private string BuildTokenizedUrl(string routeUrl, string routePrefixUrl, string areaUrl, RouteSpecification routeSpec) + private string BuildTokenizedUrl(string routeUrl, string routePrefixUrl, string areaUrl, SemanticVersion versionPrefix, RouteSpecification routeSpec) { var delimitedUrl = routeUrl + "/"; @@ -335,6 +360,13 @@ private string BuildTokenizedUrl(string routeUrl, string routePrefixUrl, string delimitedUrl = delimitedRoutePrefix + delimitedUrl; } + if (versionPrefix != null) + { + var delimitedVerisonPrefix = versionPrefix.ToString() + "/"; + if (!delimitedUrl.StartsWith(delimitedVerisonPrefix)) + delimitedUrl = delimitedVerisonPrefix + delimitedUrl; + } + // Prepend area url if available if (areaUrl.HasValue() && !routeSpec.IgnoreAreaUrl) { @@ -405,7 +437,7 @@ private static string RemoveQueryString(string url) return url; } - 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()) @@ -437,11 +469,12 @@ private IEnumerable CreateRouteTranslations(RouteSpecification // Otherwise, build a translated route var defaults = CreateRouteDefaults(routeSpec); - var constraints = CreateRouteConstraints(routeSpec); + var constraints = CreateRouteConstraints(routeSpec, version); var dataTokens = CreateRouteDataTokens(routeSpec); var routeUrl = CreateRouteUrl(translatedRouteUrl ?? routeSpec.RouteUrl, translatedRoutePrefix ?? routeSpec.RoutePrefixUrl, translatedAreaUrl ?? routeSpec.AreaUrl, + version, defaults, routeSpec); diff --git a/src/AttributeRouting/Framework/RouteReflector.cs b/src/AttributeRouting/Framework/RouteReflector.cs index a0a86b9..6aea03c 100644 --- a/src/AttributeRouting/Framework/RouteReflector.cs +++ b/src/AttributeRouting/Framework/RouteReflector.cs @@ -58,13 +58,14 @@ private IEnumerable BuildRouteSpecifications(IEnumerable(false); var routeAreaAttribute = GetRouteAreaAttribute(controllerType, convention); + var routeVersionedAttribute = GetRouteVersionedAttribute(controllerType, convention); // For each action method on the controller: var actionMethods = controllerType.GetActionMethods(inheritActionsFromBaseController); foreach (var actionMethod in actionMethods) { var routePrefixAttributes = GetRoutePrefixAttributes(controllerType, convention, actionMethod).ToList(); - + // For each route attribute on the action method: var routeAttributes = GetRouteAttributes(actionMethod, convention); foreach (var routeAttribute in routeAttributes) @@ -75,7 +76,7 @@ private IEnumerable BuildRouteSpecifications(IEnumerable BuildRouteSpecifications(IEnumerable BuildRouteSpecifications(IEnumerableAn applicable for the controller. /// The for the action. /// The route specification. - private RouteSpecification BuildRouteSpecification(int controllerIndex, Type controllerType, MethodInfo actionMethod, RouteAreaAttribute routeAreaAttribute, RoutePrefixAttribute routePrefixAttribute, IRouteAttribute routeAttribute) + private RouteSpecification BuildRouteSpecification(int controllerIndex, Type controllerType, MethodInfo actionMethod, RouteAreaAttribute routeAreaAttribute, RoutePrefixAttribute routePrefixAttribute, IRouteAttribute routeAttribute, RouteVersionedAttribute routeVersionedAttribute) { var isAsyncController = controllerType.IsAsyncController(); var subdomain = GetAreaSubdomain(routeAreaAttribute); @@ -143,6 +144,9 @@ private RouteSpecification BuildRouteSpecification(int controllerIndex, Type con SitePrecedence = GetSortableOrder(routeAttribute.SitePrecedence), Subdomain = subdomain, UseLowercaseRoute = routeAttribute.UseLowercaseRouteFlag, + IsVersioned = routeVersionedAttribute != null && routeVersionedAttribute.IsVersioned, + MinVersion = routeAttribute.MinVersion ?? (routeVersionedAttribute != null ? routeVersionedAttribute.MinVersion : null), + MaxVersion = routeAttribute.MaxVersion ?? (routeVersionedAttribute != null ? routeVersionedAttribute.MaxVersion : null) }; } @@ -307,5 +311,17 @@ private static long GetSortableOrder(int value) { return (value >= 0 ? long.MinValue : long.MaxValue) + value; } + + + /// + /// Get a to use for the controller. + /// + /// The controller type. + /// An applicable for the controller. + /// An applicable . + private static RouteVersionedAttribute GetRouteVersionedAttribute(Type controllerType, RouteConventionAttributeBase convention) + { + return controllerType.GetCustomAttribute(true); + } } } \ No newline at end of file diff --git a/src/AttributeRouting/Framework/RouteSpecification.cs b/src/AttributeRouting/Framework/RouteSpecification.cs index ed454c6..5ef766e 100644 --- a/src/AttributeRouting/Framework/RouteSpecification.cs +++ b/src/AttributeRouting/Framework/RouteSpecification.cs @@ -57,5 +57,11 @@ public class RouteSpecification public string Subdomain { get; set; } public bool? UseLowercaseRoute { 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 7595a0e..e55d5e7 100644 --- a/src/AttributeRouting/IRouteAttribute.cs +++ b/src/AttributeRouting/IRouteAttribute.cs @@ -121,5 +121,21 @@ public interface IRouteAttribute /// when building up the route URL. /// bool IgnoreAreaUrl { get; set; } + + + /// + /// 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