File: VersionRouteAttribute.cs
Web Access
Project: src\src\Mvc\test\WebSites\VersioningWebSite\VersioningWebSite.csproj (VersioningWebSite)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
 
namespace VersioningWebSite;
 
public class VersionRouteAttribute : RouteAttribute, IActionConstraintFactory
{
    private readonly IActionConstraint _actionConstraint;
 
    // 5
    // [5]
    // (5)
    // (5]
    // [5)
    // (3-5)
    // (3-5]
    // [3-5)
    // [3-5]
    // [35-56]
    // Parses the above version formats and captures lb (lower bound), range, and hb (higher bound)
    // We filter out (5), (5], [5) manually after we do the parsing.
    private static readonly Regex _versionParser = new Regex(@"^(?<lb>[\(\[])?(?<range>\d+(-\d+)?)(?<hb>[\)\]])?$");
 
    public bool IsReusable => true;
 
    public VersionRouteAttribute(string template)
        : base(template)
    {
    }
 
    public VersionRouteAttribute(string template, string versionRange)
        : base(template)
    {
        var constraint = CreateVersionConstraint(versionRange);
 
        if (constraint == null)
        {
            var message = $"Invalid version format: {versionRange}";
            throw new ArgumentException(message, "versionRange");
        }
 
        _actionConstraint = constraint;
    }
 
    private static IActionConstraint CreateVersionConstraint(string versionRange)
    {
        var match = _versionParser.Match(versionRange);
 
        if (!match.Success)
        {
            return null;
        }
 
        var lowerBound = match.Groups["lb"].Value;
        var higherBound = match.Groups["hb"].Value;
        var range = match.Groups["range"].Value;
 
        var rangeValues = range.Split('-');
        if (rangeValues.Length == 1)
        {
            return GetSingleVersionOrUnboundedHigherVersionConstraint(lowerBound, higherBound, rangeValues);
        }
        else
        {
            return GetBoundedRangeVersionConstraint(lowerBound, higherBound, rangeValues);
        }
    }
 
    private static IActionConstraint GetBoundedRangeVersionConstraint(
        string lowerBound,
        string higherBound,
        string[] rangeValues)
    {
        // [3-5, (3-5, 3-5], 3-5), 3-5 are not valid
        if (string.IsNullOrEmpty(lowerBound) || string.IsNullOrEmpty(higherBound))
        {
            return null;
        }
 
        var minVersion = int.Parse(rangeValues[0], CultureInfo.InvariantCulture);
        var maxVersion = int.Parse(rangeValues[1], CultureInfo.InvariantCulture);
 
        // Adjust min version and max version if the limit is exclusive.
        minVersion = lowerBound == "(" ? minVersion + 1 : minVersion;
        maxVersion = higherBound == ")" ? maxVersion - 1 : maxVersion;
 
        if (minVersion > maxVersion)
        {
            return null;
        }
 
        return new VersionRangeValidator(minVersion, maxVersion);
    }
 
    private static IActionConstraint GetSingleVersionOrUnboundedHigherVersionConstraint(
        string lowerBound,
        string higherBound,
        string[] rangeValues)
    {
        // (5], [5), (5), [5, (5, 5], 5) are not valid
        if (lowerBound == "(" || higherBound == ")" ||
            (string.IsNullOrEmpty(lowerBound) ^ string.IsNullOrEmpty(higherBound)))
        {
            return null;
        }
 
        var version = int.Parse(rangeValues[0], CultureInfo.InvariantCulture);
        if (!string.IsNullOrEmpty(lowerBound))
        {
            // [5]
            return new VersionRangeValidator(version, version);
        }
        else
        {
            // 5
            return new VersionRangeValidator(version, maxVersion: null);
        }
    }
 
    IActionConstraint IActionConstraintFactory.CreateInstance(IServiceProvider services)
    {
        return _actionConstraint;
    }
}