-
Notifications
You must be signed in to change notification settings - Fork 27
/
TypedRoute.cs
206 lines (182 loc) · 8.31 KB
/
TypedRoute.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using OrchardCore.Admin;
using OrchardCore.Environment.Extensions;
using OrchardCore.Modules.Manifest;
using OrchardCore.Mvc.Core.Utilities;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Reflection;
namespace Lombiq.HelpfulLibraries.OrchardCore.Mvc;
public class TypedRoute
{
private static readonly ConcurrentDictionary<string, TypedRoute> _cache = new();
private readonly string _area;
private readonly Type _controller;
private readonly MethodInfo _action;
private readonly List<KeyValuePair<string, string>> _arguments;
private readonly Lazy<bool> _isAdminLazy;
private readonly Lazy<string> _routeLazy;
public TypedRoute(
MethodInfo action,
IEnumerable<KeyValuePair<string, string>> arguments,
ITypeFeatureProvider typeFeatureProvider = null)
{
if (action?.DeclaringType is not { } controller)
{
throw new InvalidOperationException(
$"{action?.Name ?? nameof(action)}'s {nameof(action.DeclaringType)} cannot be null.");
}
_controller = controller;
_action = action;
_arguments = arguments is List<KeyValuePair<string, string>> list ? list : arguments.ToList();
var areaPair = _arguments.Find(pair => pair.Key.EqualsOrdinalIgnoreCase("area"));
if (areaPair.Value is { } areaArgumentValue)
{
_area = areaArgumentValue;
_arguments.Remove(areaPair);
}
else
{
_area = typeFeatureProvider?.GetFeatureForDependency(controller).Extension.Id ??
controller.Assembly.GetCustomAttribute<ModuleNameAttribute>()?.Name ??
controller.Assembly.GetCustomAttribute<ModuleMarkerAttribute>()?.Name ??
throw new InvalidOperationException(
"No area argument was provided and couldn't figure out the module technical name. Are " +
"you sure this controller belongs to an Orchard Core module?");
}
_isAdminLazy = new Lazy<bool>(() =>
controller.GetCustomAttribute<AdminAttribute>() != null ||
action.GetCustomAttribute<AdminAttribute>() != null);
_routeLazy = new Lazy<string>(() =>
action.GetCustomAttribute<RouteAttribute>()?.Template is { } route && !string.IsNullOrWhiteSpace(route)
? GetRoute(route)
: $"{_area}/{controller.ControllerName()}/{action.GetCustomAttribute<ActionNameAttribute>()?.Name ?? action.Name}");
}
/// <summary>
/// Creates a relative URL based on the given <paramref name="httpContext"/>, the area, and the names of the
/// <c>action</c> and the <c>controller</c>.
/// </summary>
public string ToString(HttpContext httpContext)
{
var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
var arguments = new RouteValueDictionary(_arguments) { ["area"] = _area };
return linkGenerator.GetUriByAction(
httpContext,
_action.Name,
_controller.ControllerName(),
arguments);
}
/// <summary>
/// Creates a local URL using a prefix, the current route, and other arguments.
/// </summary>
public override string ToString()
{
var prefix = _isAdminLazy.Value ? "/Admin/" : "/";
var route = _routeLazy.Value;
var arguments = _arguments.Any()
? "?" + string.Join('&', _arguments.Select((key, value) => $"{key}={WebUtility.UrlEncode(value)}"))
: string.Empty;
return prefix + route + arguments;
}
/// <summary>
/// Creates a local URL on a tenant using the provided <paramref name="tenantName"/>. If
/// <paramref name="tenantName"/> is empty or "<c>Default</c>", creates a local URL using a prefix, the current
/// route, and other arguments.
/// </summary>
public string ToString(string tenantName) =>
string.IsNullOrWhiteSpace(tenantName) || tenantName.EqualsOrdinalIgnoreCase("Default")
? ToString()
: $"/{tenantName}{this}";
private string GetRoute(string route)
{
var argumentsCopy = _arguments.ToList(); // This way modifying _arguments in the loop won't cause problems.
foreach (var (name, value) in argumentsCopy)
{
var placeholder = $"{{{name}}}";
if (!route.ContainsOrdinalIgnoreCase(placeholder)) continue;
route = route.ReplaceOrdinalIgnoreCase(placeholder, WebUtility.UrlEncode(value));
_arguments.RemoveAll(pair => pair.Key == name);
}
return route;
}
public static implicit operator RouteValueDictionary(TypedRoute route) =>
new(route._arguments)
{
["area"] = route._area,
["controller"] = route._controller.ControllerName(),
["action"] = route._action.Name,
};
/// <summary>
/// Creates and returns a new <see cref="TypedRoute"/> using the provided <paramref name="actionExpression"/>,
/// also adding it to the cache.
/// </summary>
/// <param name="actionExpression">The action expression whose arguments are used for the process.</param>
/// <param name="additionalArguments">Additional arguments to add to the route and the key in the cache.</param>
public static TypedRoute CreateFromExpression<TController>(
Expression<Action<TController>> actionExpression,
IEnumerable<(string Key, object Value)> additionalArguments,
ITypeFeatureProvider typeFeatureProvider = null)
where TController : ControllerBase =>
CreateFromExpression(
actionExpression,
additionalArguments.Select((key, value) => new KeyValuePair<string, string>(key, value.ToString())),
typeFeatureProvider);
/// <summary>
/// Creates and returns a new <see cref="TypedRoute"/> using the provided <paramref name="action"/> expression,
/// also adding it to the cache.
/// </summary>
/// <param name="action">The action expression whose arguments are used for the process.</param>
/// <param name="additionalArguments">Additional arguments to add to the route and the key in the cache.</param>
public static TypedRoute CreateFromExpression<TController>(
Expression<Action<TController>> action,
IEnumerable<KeyValuePair<string, string>> additionalArguments,
ITypeFeatureProvider typeFeatureProvider = null)
where TController : ControllerBase
{
Expression actionExpression = action;
while (actionExpression is LambdaExpression { Body: not MethodCallExpression } lambdaExpression)
{
actionExpression = lambdaExpression.Body;
}
var operation = (MethodCallExpression)((LambdaExpression)actionExpression).Body;
var methodParameters = operation.Method.GetParameters();
var arguments = operation
.Arguments
.Select((argument, index) => new KeyValuePair<string, string>(
methodParameters[index].Name,
ValueToString(Expression.Lambda(argument).Compile().DynamicInvoke())))
.Where(pair => pair.Value != null)
.Concat(additionalArguments)
.ToList();
var key = string.Join(
separator: '|',
typeof(TController),
operation.Method,
string.Join(',', arguments.Select(pair => $"{pair.Key}={pair.Value}")));
return _cache.GetOrAdd(
key,
_ => new TypedRoute(
operation.Method,
arguments,
typeFeatureProvider));
}
private static string ValueToString(object value) =>
value switch
{
null => null,
string text => text,
DateTime date => date.ToString("s", CultureInfo.InvariantCulture),
byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal =>
string.Format(CultureInfo.InvariantCulture, "{0}", value),
_ => JsonConvert.SerializeObject(value),
};
}