// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Matching;
namespace Microsoft.AspNetCore.Routing.Patterns
{
///
/// Contains factory methods for creating and related types.
/// Use to parse a route pattern in
/// string format.
///
public static class RoutePatternFactory
{
private static readonly IReadOnlyDictionary EmptyDefaultsDictionary =
new ReadOnlyDictionary(new Dictionary());
private static readonly IReadOnlyDictionary> EmptyPoliciesDictionary =
new ReadOnlyDictionary>(new Dictionary>());
///
/// Creates a from its string representation.
///
/// The route pattern string to parse.
/// The .
public static RoutePattern Parse(string pattern)
{
if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
}
return RoutePatternParser.Parse(pattern);
}
///
/// Creates a from its string representation along
/// with provided default values and parameter policies.
///
/// The route pattern string to parse.
///
/// Additional default values to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the parsed route pattern.
///
///
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the parsed route pattern.
///
/// The .
public static RoutePattern Parse(string pattern, object defaults, object parameterPolicies)
{
if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
}
var original = RoutePatternParser.Parse(pattern);
return Pattern(original.RawText, defaults, parameterPolicies, original.PathSegments);
}
///
/// Creates a new from a collection of segments.
///
/// The collection of segments.
/// The .
public static RoutePattern Pattern(IEnumerable segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, null, null, segments);
}
///
/// Creates a new from a collection of segments.
///
/// The raw text to associate with the route pattern. May be null.
/// The collection of segments.
/// The .
public static RoutePattern Pattern(string rawText, IEnumerable segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, null, null, segments);
}
///
/// Creates a from a collection of segments along
/// with provided default values and parameter policies.
///
///
/// Additional default values to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
///
///
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
///
/// The collection of segments.
/// The .
public static RoutePattern Pattern(
object defaults,
object parameterPolicies,
IEnumerable segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
}
///
/// Creates a from a collection of segments along
/// with provided default values and parameter policies.
///
/// The raw text to associate with the route pattern.
///
/// Additional default values to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
///
///
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
///
/// The collection of segments.
/// The .
public static RoutePattern Pattern(
string rawText,
object defaults,
object parameterPolicies,
IEnumerable segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
}
///
/// Creates a new from a collection of segments.
///
/// The collection of segments.
/// The .
public static RoutePattern Pattern(params RoutePatternPathSegment[] segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, null, null, segments);
}
///
/// Creates a new from a collection of segments.
///
/// The raw text to associate with the route pattern. May be null.
/// The collection of segments.
/// The .
public static RoutePattern Pattern(string rawText, params RoutePatternPathSegment[] segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, null, null, segments);
}
///
/// Creates a from a collection of segments along
/// with provided default values and parameter policies.
///
///
/// Additional default values to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
///
///
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
///
/// The collection of segments.
/// The .
public static RoutePattern Pattern(
object defaults,
object parameterPolicies,
params RoutePatternPathSegment[] segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
}
///
/// Creates a from a collection of segments along
/// with provided default values and parameter policies.
///
/// The raw text to associate with the route pattern.
///
/// Additional default values to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
///
///
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using
/// and then merged into the route pattern.
///
/// The collection of segments.
/// The .
public static RoutePattern Pattern(
string rawText,
object defaults,
object parameterPolicies,
params RoutePatternPathSegment[] segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
}
private static RoutePattern PatternCore(
string rawText,
RouteValueDictionary defaults,
RouteValueDictionary parameterPolicies,
IEnumerable segments)
{
// We want to merge the segment data with the 'out of line' defaults and parameter policies.
//
// This means that for parameters that have 'out of line' defaults we will modify
// the parameter to contain the default (same story for parameter policies).
//
// We also maintain a collection of defaults and parameter policies that will also
// contain the values that don't match a parameter.
//
// It's important that these two views of the data are consistent. We don't want
// values specified out of line to have a different behavior.
Dictionary updatedDefaults = null;
if (defaults != null && defaults.Count > 0)
{
updatedDefaults = new Dictionary(defaults.Count, StringComparer.OrdinalIgnoreCase);
foreach (var kvp in defaults)
{
updatedDefaults.Add(kvp.Key, kvp.Value);
}
}
Dictionary> updatedParameterPolicies = null;
if (parameterPolicies != null && parameterPolicies.Count > 0)
{
updatedParameterPolicies = new Dictionary>(parameterPolicies.Count, StringComparer.OrdinalIgnoreCase);
foreach (var kvp in parameterPolicies)
{
updatedParameterPolicies.Add(kvp.Key, new List()
{
kvp.Value is IParameterPolicy parameterPolicy
? ParameterPolicy(parameterPolicy)
: Constraint(kvp.Value), // Constraint will convert string values into regex constraints
});
}
}
List parameters = null;
var updatedSegments = segments.ToArray();
for (var i = 0; i < updatedSegments.Length; i++)
{
var segment = VisitSegment(updatedSegments[i]);
updatedSegments[i] = segment;
for (var j = 0; j < segment.Parts.Count; j++)
{
if (segment.Parts[j] is RoutePatternParameterPart parameter)
{
if (parameters == null)
{
parameters = new List();
}
parameters.Add(parameter);
}
}
}
return new RoutePattern(
rawText,
updatedDefaults ?? EmptyDefaultsDictionary,
updatedParameterPolicies != null
? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray())
: EmptyPoliciesDictionary,
(IReadOnlyList)parameters ?? Array.Empty(),
updatedSegments);
RoutePatternPathSegment VisitSegment(RoutePatternPathSegment segment)
{
RoutePatternPart[] updatedParts = null;
for (var i = 0; i < segment.Parts.Count; i++)
{
var part = segment.Parts[i];
var updatedPart = VisitPart(part);
if (part != updatedPart)
{
if (updatedParts == null)
{
updatedParts = segment.Parts.ToArray();
}
updatedParts[i] = updatedPart;
}
}
if (updatedParts == null)
{
// Segment has not changed
return segment;
}
return new RoutePatternPathSegment(updatedParts);
}
RoutePatternPart VisitPart(RoutePatternPart part)
{
if (!part.IsParameter)
{
return part;
}
var parameter = (RoutePatternParameterPart)part;
var @default = parameter.Default;
if (updatedDefaults != null && updatedDefaults.TryGetValue(parameter.Name, out var newDefault))
{
if (parameter.Default != null && !Equals(newDefault, parameter.Default))
{
var message = Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(parameter.Name);
throw new InvalidOperationException(message);
}
if (parameter.IsOptional)
{
var message = Resources.TemplateRoute_OptionalCannotHaveDefaultValue;
throw new InvalidOperationException(message);
}
@default = newDefault;
}
if (parameter.Default != null)
{
if (updatedDefaults == null)
{
updatedDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase);
}
updatedDefaults[parameter.Name] = parameter.Default;
}
List parameterConstraints = null;
if ((updatedParameterPolicies == null || !updatedParameterPolicies.TryGetValue(parameter.Name, out parameterConstraints)) &&
parameter.ParameterPolicies.Count > 0)
{
if (updatedParameterPolicies == null)
{
updatedParameterPolicies = new Dictionary>(StringComparer.OrdinalIgnoreCase);
}
parameterConstraints = new List();
updatedParameterPolicies.Add(parameter.Name, parameterConstraints);
}
if (parameter.ParameterPolicies.Count > 0)
{
parameterConstraints.AddRange(parameter.ParameterPolicies);
}
if (Equals(parameter.Default, @default)
&& parameter.ParameterPolicies.Count == 0
&& (parameterConstraints?.Count ?? 0) == 0)
{
// Part has not changed
return part;
}
return ParameterPartCore(
parameter.Name,
@default,
parameter.ParameterKind,
(IEnumerable)parameterConstraints ?? Array.Empty(),
parameter.EncodeSlashes);
}
}
///
/// Creates a from the provided collection
/// of parts.
///
/// The collection of parts.
/// The .
public static RoutePatternPathSegment Segment(IEnumerable parts)
{
if (parts == null)
{
throw new ArgumentNullException(nameof(parts));
}
return SegmentCore(parts);
}
///
/// Creates a from the provided collection
/// of parts.
///
/// The collection of parts.
/// The .
public static RoutePatternPathSegment Segment(params RoutePatternPart[] parts)
{
if (parts == null)
{
throw new ArgumentNullException(nameof(parts));
}
return SegmentCore(parts);
}
private static RoutePatternPathSegment SegmentCore(IEnumerable parts)
{
return new RoutePatternPathSegment(parts.ToArray());
}
///
/// Creates a from the provided text
/// content.
///
/// The text content.
/// The .
public static RoutePatternLiteralPart LiteralPart(string content)
{
if (string.IsNullOrEmpty(content))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content));
}
if (content.IndexOf('?') >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidLiteral(content));
}
return LiteralPartCore(content);
}
private static RoutePatternLiteralPart LiteralPartCore(string content)
{
return new RoutePatternLiteralPart(content);
}
///
/// Creates a from the provided text
/// content.
///
/// The text content.
/// The .
public static RoutePatternSeparatorPart SeparatorPart(string content)
{
if (string.IsNullOrEmpty(content))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content));
}
return SeparatorPartCore(content);
}
private static RoutePatternSeparatorPart SeparatorPartCore(string content)
{
return new RoutePatternSeparatorPart(content);
}
///
/// Creates a from the provided parameter name.
///
/// The parameter name.
/// The .
public static RoutePatternParameterPart ParameterPart(string parameterName)
{
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
}
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
}
return ParameterPartCore(
parameterName: parameterName,
@default: null,
parameterKind: RoutePatternParameterKind.Standard,
parameterPolicies: Array.Empty());
}
///
/// Creates a from the provided parameter name
/// and default value.
///
/// The parameter name.
/// The parameter default value. May be null.
/// The .
public static RoutePatternParameterPart ParameterPart(string parameterName, object @default)
{
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
}
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
}
return ParameterPartCore(
parameterName: parameterName,
@default: @default,
parameterKind: RoutePatternParameterKind.Standard,
parameterPolicies: Array.Empty());
}
///
/// Creates a from the provided parameter name
/// and default value, and parameter kind.
///
/// The parameter name.
/// The parameter default value. May be null.
/// The parameter kind.
/// The .
public static RoutePatternParameterPart ParameterPart(
string parameterName,
object @default,
RoutePatternParameterKind parameterKind)
{
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
}
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
}
if (@default != null && parameterKind == RoutePatternParameterKind.Optional)
{
throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind));
}
return ParameterPartCore(
parameterName: parameterName,
@default: @default,
parameterKind: parameterKind,
parameterPolicies: Array.Empty());
}
///
/// Creates a from the provided parameter name
/// and default value, parameter kind, and parameter policies.
///
/// The parameter name.
/// The parameter default value. May be null.
/// The parameter kind.
/// The parameter policies to associated with the parameter.
/// The .
public static RoutePatternParameterPart ParameterPart(
string parameterName,
object @default,
RoutePatternParameterKind parameterKind,
IEnumerable parameterPolicies)
{
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
}
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
}
if (@default != null && parameterKind == RoutePatternParameterKind.Optional)
{
throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind));
}
if (parameterPolicies == null)
{
throw new ArgumentNullException(nameof(parameterPolicies));
}
return ParameterPartCore(
parameterName: parameterName,
@default: @default,
parameterKind: parameterKind,
parameterPolicies: parameterPolicies);
}
///
/// Creates a from the provided parameter name
/// and default value, parameter kind, and parameter policies.
///
/// The parameter name.
/// The parameter default value. May be null.
/// The parameter kind.
/// The parameter policies to associated with the parameter.
/// The .
public static RoutePatternParameterPart ParameterPart(
string parameterName,
object @default,
RoutePatternParameterKind parameterKind,
params RoutePatternParameterPolicyReference[] parameterPolicies)
{
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
}
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
}
if (@default != null && parameterKind == RoutePatternParameterKind.Optional)
{
throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind));
}
if (parameterPolicies == null)
{
throw new ArgumentNullException(nameof(parameterPolicies));
}
return ParameterPartCore(
parameterName: parameterName,
@default: @default,
parameterKind: parameterKind,
parameterPolicies: parameterPolicies);
}
private static RoutePatternParameterPart ParameterPartCore(
string parameterName,
object @default,
RoutePatternParameterKind parameterKind,
IEnumerable parameterPolicies)
{
return ParameterPartCore(parameterName, @default, parameterKind, parameterPolicies, encodeSlashes: true);
}
private static RoutePatternParameterPart ParameterPartCore(
string parameterName,
object @default,
RoutePatternParameterKind parameterKind,
IEnumerable parameterPolicies,
bool encodeSlashes)
{
return new RoutePatternParameterPart(
parameterName,
@default,
parameterKind,
parameterPolicies.ToArray(),
encodeSlashes);
}
///
/// Creates a from the provided contraint.
///
///
/// The constraint object, which must be of type
/// or . If the constraint object is a
/// then it will be tranformed into an instance of .
///
/// The .
public static RoutePatternParameterPolicyReference Constraint(object constraint)
{
// Similar to RouteConstraintBuilder
if (constraint is IRouteConstraint policy)
{
return ParameterPolicyCore(policy);
}
else if (constraint is string content)
{
return ParameterPolicyCore(new RegexRouteConstraint("^(" + content + ")$"));
}
else
{
throw new InvalidOperationException(Resources.FormatRoutePattern_InvalidConstraintReference(
constraint ?? "null",
typeof(IRouteConstraint)));
}
}
///
/// Creates a from the provided constraint.
///
///
/// The constraint object.
///
/// The .
public static RoutePatternParameterPolicyReference Constraint(IRouteConstraint constraint)
{
if (constraint == null)
{
throw new ArgumentNullException(nameof(constraint));
}
return ParameterPolicyCore(constraint);
}
///
/// Creates a from the provided constraint.
///
///
/// The constraint text, which will be resolved by .
///
/// The .
public static RoutePatternParameterPolicyReference Constraint(string constraint)
{
if (string.IsNullOrEmpty(constraint))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(constraint));
}
return ParameterPolicyCore(constraint);
}
///
/// Creates a from the provided object.
///
///
/// The parameter policy object.
///
/// The .
public static RoutePatternParameterPolicyReference ParameterPolicy(IParameterPolicy parameterPolicy)
{
if (parameterPolicy == null)
{
throw new ArgumentNullException(nameof(parameterPolicy));
}
return ParameterPolicyCore(parameterPolicy);
}
///
/// Creates a from the provided object.
///
///
/// The parameter policy text, which will be resolved by .
///
/// The .
public static RoutePatternParameterPolicyReference ParameterPolicy(string parameterPolicy)
{
if (string.IsNullOrEmpty(parameterPolicy))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterPolicy));
}
return ParameterPolicyCore(parameterPolicy);
}
private static RoutePatternParameterPolicyReference ParameterPolicyCore(string parameterPolicy)
{
return new RoutePatternParameterPolicyReference(parameterPolicy);
}
private static RoutePatternParameterPolicyReference ParameterPolicyCore(IParameterPolicy parameterPolicy)
{
return new RoutePatternParameterPolicyReference(parameterPolicy);
}
}
}