/
RouteLinker.cs
464 lines (439 loc) · 23 KB
/
RouteLinker.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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Net.Http;
using System.Reflection;
using System.Web.Http.Controllers;
using System.Web.Http.Routing;
using System.Web.Http.Hosting;
using System.Globalization;
using System.Threading.Tasks;
namespace Ploeh.Hyprlinkr
{
/// <summary>
/// Creates URIs from type-safe expressions, based on routing configuration.
/// </summary>
/// <remarks>
/// <para>
/// The purpose of this class is to create correct URIs to other resources within an ASP.NET
/// Web API solution. Instead of hard-coding URIs or building them from hard-coded URI
/// templates which may go out of sync with the routes defined in an
/// <see cref="System.Web.Http.HttpRouteCollection" />, the RouteLinker class provides a method
/// where URIs can be built from the routes defined in the route collection.
/// </para>
/// </remarks>
/// <seealso cref="GetUri{T}(Expression{Action{T}})" />
public class RouteLinker : IResourceLinker
{
private readonly HttpRequestMessage request;
private readonly IRouteValuesQuery valuesQuery;
private readonly IRouteDispatcher dispatcher;
/// <summary>
/// Initializes a new instance of the <see cref="RouteLinker"/> class.
/// </summary>
/// <param name="request">The current request.</param>
/// <remarks>
/// <para>
/// After initialization, the <paramref name="request" /> value is available through the
/// <see cref="Request" /> property.
/// </para>
/// </remarks>
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteDispatcher)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteValuesQuery)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteValuesQuery, IRouteDispatcher)" />
public RouteLinker(HttpRequestMessage request)
: this(request, new DefaultRouteDispatcher())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RouteLinker" /> class.
/// </summary>
/// <param name="request">The current request.</param>
/// <param name="routeValuesQuery">
/// A Strategy for extracting route values.
/// </param>
/// <remarks>
/// <para>
/// This constructor overload requires a custom
/// <see cref="IRouteValuesQuery" />. If you don't want to use a custom
/// query, you can use the simpler constructor overload.
/// </para>
/// <para>
/// After initialization, the <paramref name="request" /> value is
/// available through the <see cref="Request" /> property, and the
/// <paramref name="routeValuesQuery" /> value is available via the
/// <see cref="RouteValuesQuery" /> property.
/// </para>
/// </remarks>
/// <seealso cref="RouteLinker(HttpRequestMessage)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteDispatcher)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteValuesQuery, IRouteDispatcher)" />
public RouteLinker(HttpRequestMessage request, IRouteValuesQuery routeValuesQuery)
: this(request, routeValuesQuery, new DefaultRouteDispatcher())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RouteLinker"/> class.
/// </summary>
/// <param name="request">The current request.</param>
/// <param name="dispatcher">A custom dispatcher.</param>
/// <remarks>
/// <para>
/// This constructor overload requires a custom <see cref="IRouteDispatcher" />. If you
/// don't want to use a custom dispatcher, you can use the simpler constructor overload.
/// </para>
/// <para>
/// After initialization, the <paramref name="request" /> value is available through the
/// <see cref="Request" /> property; and the <paramref name="dispatcher" /> is available
/// through the <see cref="RouteDispatcher" /> property.
/// </para>
/// </remarks>
/// <seealso cref="RouteLinker(HttpRequestMessage)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteValuesQuery)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteValuesQuery, IRouteDispatcher)" />
public RouteLinker(HttpRequestMessage request, IRouteDispatcher dispatcher)
: this(request, new ScalarRouteValuesQuery(), dispatcher)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RouteLinker" /> class.
/// </summary>
/// <param name="request">The current request.</param>
/// <param name="routeValuesQuery">
/// A Strategy for extracting route values.
/// </param>
/// <param name="dispatcher">A custom dispatcher.</param>
/// <remarks>
/// <para>
/// This constructor overload requires custom Strategies to be
/// injected. If you don't want to supply one or both custom
/// Strategies, you can use a simpler constructor overload.
/// </para>
/// <para>
/// After initialization, the parameter values are available as
/// read-only properties.
/// </para>
/// </remarks>
/// <exception cref="System.ArgumentNullException">
/// request is null
/// </exception>
/// <exception cref="System.ArgumentNullException">
/// routeValuesQuery is null
/// </exception>
/// <exception cref="System.ArgumentNullException">
/// dispatcher is null
/// </exception>
/// <seealso cref="RouteLinker(HttpRequestMessage)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteValuesQuery)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteDispatcher)" />
/// <seealso cref="Request" />
/// <see cref="RouteValuesQuery" />
/// <see cref="RouteDispatcher" />
public RouteLinker(
HttpRequestMessage request,
IRouteValuesQuery routeValuesQuery,
IRouteDispatcher dispatcher)
{
if (request == null)
throw new ArgumentNullException("request");
if (routeValuesQuery == null)
throw new ArgumentNullException("routeValuesQuery");
if (dispatcher == null)
throw new ArgumentNullException("dispatcher");
this.request = request;
this.valuesQuery = routeValuesQuery;
this.dispatcher = dispatcher;
}
/// <summary>
/// Creates an URI based on a type-safe expression.
/// </summary>
/// <typeparam name="T">
/// The type of resource to link to. This will typically be the type of an
/// <see cref="System.Web.Http.ApiController" />, but doesn't have to be.
/// </typeparam>
/// <typeparam name="TResult">
/// The return type of the Action Method of the resource.
/// </typeparam>
/// <param name="method">
/// An expression wich identifies the action method that serves the desired resource.
/// </param>
/// <returns>
/// An <see cref="Uri" /> instance which represents the resource identifed by
/// <paramref name="method" />.
/// </returns>
/// <remarks>
/// <para>
/// This method is used to build valid URIs for resources represented by code. In the
/// ASP.NET Web API, resources are served by Action Methods on Controllers. If building a
/// REST service with hypermedia controls, you will want to create links to various other
/// resources in your service. Viewed from code, these resources are encapsulated by Action
/// Methods, but you need to build valid URIs that, when requested via HTTP, invokes the
/// desired Action Method.
/// </para>
/// <para>
/// The target Action Method can be type-safely identified by the
/// <paramref name="method" /> expression. The <typeparamref name="T" /> type argument will
/// typically indicate a particular class which derives from
/// <see cref="System.Web.Http.ApiController" />, but there's no generic constraint on the
/// type argument, so this is not required.
/// </para>
/// <para>
/// Based on the Action Method identified by the supplied expression, the ASP.NET Web API
/// routing configuration is consulted to build an appropriate URI which matches the Action
/// Method. The routing configuration is pulled from the <see cref="HttpRequestMessage" />
/// instance supplied to the constructor of the <see cref="RouteLinker" /> class.
/// </para>
/// <para>
/// This overload mostly exists to support F# clients.
/// </para>
/// </remarks>
/// <seealso cref="RouteLinker(HttpRequestMessage)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteDispatcher)" />
/// <example>
/// This example demonstrates how an F# client can create an <see cref="Uri" /> instance for a GetById
/// method defined on a FooController class.
/// <code>
/// let uri = linker.GetUri(fun (c : FooController) -> c.GetById(1337))
/// </code>
/// Given the default API route configuration, the resulting URI will be something like
/// this (assuming that the base URI is http://localhost): http://localhost/api/foo/1337
/// </example>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "The expression is strongly typed in order to prevent the caller from passing any sort of expression. It doesn't fully capture everything the caller might throw at it, but it does constrain the caller as well as possible. This enables the developer to get a compile-time exception instead of a run-time exception in most cases where an invalid expression is being supplied.")]
public Uri GetUri<T, TResult>(Expression<Func<T, TResult>> method)
{
var methodCallExp = method.GetMethodCallExpression();
return this.GetUri(methodCallExp);
}
/// <summary>
/// Creates an URI based on a type-safe expression.
/// </summary>
/// <typeparam name="T">
/// The type of resource to link to. This will typically be the type of an
/// <see cref="System.Web.Http.ApiController" />, but doesn't have to be.
/// </typeparam>
/// <param name="method">
/// An expression wich identifies the action method that serves the desired resource.
/// </param>
/// <returns>
/// An <see cref="Uri" /> instance which represents the resource identifed by
/// <paramref name="method" />.
/// </returns>
/// <remarks>
/// <para>
/// This method is used to build valid URIs for resources represented by code. In the
/// ASP.NET Web API, resources are served by Action Methods on Controllers. If building a
/// REST service with hypermedia controls, you will want to create links to various other
/// resources in your service. Viewed from code, these resources are encapsulated by Action
/// Methods, but you need to build valid URIs that, when requested via HTTP, invokes the
/// desired Action Method.
/// </para>
/// <para>
/// The target Action Method can be type-safely identified by the
/// <paramref name="method" /> expression. The <typeparamref name="T" /> type argument will
/// typically indicate a particular class which derives from
/// <see cref="System.Web.Http.ApiController" />, but there's no generic constraint on the
/// type argument, so this is not required.
/// </para>
/// <para>
/// Based on the Action Method identified by the supplied expression, the ASP.NET Web API
/// routing configuration is consulted to build an appropriate URI which matches the Action
/// Method. The routing configuration is pulled from the <see cref="HttpRequestMessage" />
/// instance supplied to the constructor of the <see cref="RouteLinker" /> class.
/// </para>
/// </remarks>
/// <seealso cref="RouteLinker(HttpRequestMessage)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteDispatcher)" />
/// <example>
/// This example demonstrates how to create an <see cref="Uri" /> instance for a GetById
/// method defined on a FooController class.
/// <code>
/// var uri = linker.GetUri<FooController>(r => r.GetById(1337));
/// </code>
/// Given the default API route configuration, the resulting URI will be something like
/// this (assuming that the base URI is http://localhost): http://localhost/api/foo/1337
/// </example>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "The expression is strongly typed in order to prevent the caller from passing any sort of expression. It doesn't fully capture everything the caller might throw at it, but it does constrain the caller as well as possible. This enables the developer to get a compile-time exception instead of a run-time exception in most cases where an invalid expression is being supplied.")]
public Uri GetUri<T>(Expression<Action<T>> method)
{
var methodCallExp = method.GetMethodCallExpression();
return this.GetUri(methodCallExp);
}
/// <summary>
/// Creates an URI based on a type-safe expression.
/// </summary>
/// <typeparam name="T">
/// The type of resource to link to. This will typically be the type of
/// an <see cref="System.Web.Http.ApiController" />, but doesn't have
/// to be.
/// </typeparam>
/// <typeparam name="TResult">
/// The return type of the Action Method of the resource.
/// </typeparam>
/// <param name="method">
/// An expression wich identifies the action method that serves the
/// desired resource.
/// </param>
/// <returns>
/// A <see cref="Task{Uri}" /> instance which represents the resource
/// identifed by <paramref name="method" />.
/// </returns>
/// <remarks>
/// <para>
/// This method is used to build valid URIs for resources represented
/// by code. In the ASP.NET Web API, resources are served by Action
/// Methods on Controllers. If building a REST service with hypermedia
/// controls, you will want to create links to various other resources
/// in your service. Viewed from code, these resources are encapsulated
/// by Action Methods, but you need to build valid URIs that, when
/// requested via HTTP, invokes the desired Action Method.
/// </para>
/// <para>
/// The target Action Method can be type-safely identified by the
/// <paramref name="method" /> expression.
/// The <typeparamref name="T" /> type argument will typically indicate
/// a particular class which derives from
/// <see cref="System.Web.Http.ApiController" />, but there's no
/// generic constraint on the type argument, so this is not required.
/// </para>
/// <para>
/// Based on the Action Method identified by the supplied expression,
/// the ASP.NET Web API routing configuration is consulted to build an
/// appropriate URI which matches the Action Method. The routing
/// configuration is pulled from the <see cref="HttpRequestMessage" />
/// instance supplied to the constructor of the
/// <see cref="RouteLinker" /> class.
/// </para>
/// <para>
/// This overload supports extracting valid URI instances from async
/// Controllers.
/// </para>
/// </remarks>
/// <seealso cref="RouteLinker(HttpRequestMessage)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteDispatcher)" />
/// <seealso cref="GetUri{T}(Expression{Action{T}})" />
/// <seealso cref="GetUri{T, TResult}(Expression{Func{T, TResult}})" />
/// <exception cref="System.ArgumentNullException">method is null</exception>
/// <exception cref="System.ArgumentException">The expression's body isn't a MethodCallExpression. The code block supplied should invoke a method.\nExample: x => x.Foo().</exception>
/// <example>
/// This example demonstrates how to create an <see cref="Uri" />
/// instance for a Get method defined on an AsyncController class.
/// <code>
/// Uri actual = linker.GetUriAsync((AsyncController c) => c.Get(id)).Result;
/// </code>
/// Given the default API route configuration, the resulting URI will
/// be something like this (assuming that the base URI is
/// http://localhost): http://localhost/api/async/1337
/// </example>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "The expression is strongly typed in order to prevent the caller from passing any sort of expression. It doesn't fully capture everything the caller might throw at it, but it does constrain the caller as well as possible. This enables the developer to get a compile-time exception instead of a run-time exception in most cases where an invalid expression is being supplied.")]
public Task<Uri> GetUriAsync<T, TResult>(Expression<Func<T, Task<TResult>>> method)
{
var methodCallExp = method.GetMethodCallExpression();
return Task.Factory.StartNew(() => this.GetUri(methodCallExp));
}
private Uri GetUri(MethodCallExpression methodCallExp)
{
var r = this.Dispatch(methodCallExp);
var relativeUri = this.GetRelativeUri(r);
var baseUri = this.GetBaseUri();
return new Uri(baseUri, relativeUri);
}
private Rouple Dispatch(MethodCallExpression methodCallExp)
{
var routeValues = this.valuesQuery.GetRouteValues(methodCallExp);
return this.dispatcher.Dispatch(methodCallExp, routeValues);
}
#region CA suppressions
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "GetUri", Justification = "Workaround for a bug in CA: https://connect.microsoft.com/VisualStudio/feedback/details/521030/")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "IDictionary", Justification = "Workaround for a bug in CA: https://connect.microsoft.com/VisualStudio/feedback/details/521030/")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "UrlHelper", Justification = "Workaround for a bug in CA: https://connect.microsoft.com/VisualStudio/feedback/details/521030/")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "RouteLinker", Justification = "Workaround for a bug in CA: https://connect.microsoft.com/VisualStudio/feedback/details/521030/")]
#endregion
private Uri GetRelativeUri(Rouple r)
{
var urlHelper = this.CreateUrlHelper();
var relativeUri = urlHelper.Route(r.RouteName, r.RouteValues);
if (relativeUri == null)
throw new InvalidOperationException(
string.Format(
CultureInfo.CurrentCulture,
"The route string returned by System.Web.Http.Routing.UrlHelper.Route(string, IDictionary<string, object>) is null, which indicates an error. This can happen if the Action Method identified by the RouteLinker.GetUri method doesn't have a matching route with the name \"{0}\", or if the route parameter names don't match the method arguments.",
r.RouteName));
return new Uri(relativeUri, UriKind.Relative);
}
private Uri GetBaseUri()
{
var authority =
this.request.RequestUri.GetLeftPart(UriPartial.Authority);
return new Uri(authority);
}
private UrlHelper CreateUrlHelper()
{
return this.CopyRequestWithoutRouteValues().GetUrlHelper();
}
private HttpRequestMessage CopyRequestWithoutRouteValues()
{
var requestCopy = new HttpRequestMessage(
this.request.Method,
this.request.RequestUri);
try
{
CopyPropertiesOfCurrentRequestTo(requestCopy);
}
catch
{
requestCopy.Dispose();
throw;
}
return requestCopy;
}
private void CopyPropertiesOfCurrentRequestTo(HttpRequestMessage destination)
{
CopyNonRouteValuePropertiesFromRequestToCopy(destination);
CopyRouteDataToRequest(destination);
}
private void CopyNonRouteValuePropertiesFromRequestToCopy(HttpRequestMessage destination)
{
foreach (var kvp in this.request.Properties)
if (kvp.Key != HttpPropertyKeys.HttpRouteDataKey)
destination.Properties.Add(kvp.Key, kvp.Value);
}
private void CopyRouteDataToRequest(HttpRequestMessage requestCopy)
{
var routeData = GetRouteDataOrThrowException();
requestCopy.Properties.Add(
HttpPropertyKeys.HttpRouteDataKey,
new HttpRouteData(routeData.Route));
}
private IHttpRouteData GetRouteDataOrThrowException()
{
var routeData = this.request.GetRouteData();
if (routeData == null)
throw new InvalidOperationException("Current request has no route data.");
return routeData;
}
/// <summary>
/// Gets the request that this instance uses to create URIs.
/// </summary>
/// <seealso cref="RouteLinker(HttpRequestMessage)" />
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteDispatcher)" />
public HttpRequestMessage Request
{
get { return this.request; }
}
/// <summary>Gets the route values query.</summary>
public IRouteValuesQuery RouteValuesQuery
{
get { return this.valuesQuery; }
}
/// <summary>
/// Gets the route dispatcher.
/// </summary>
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteDispatcher)" />
public IRouteDispatcher RouteDispatcher
{
get { return this.dispatcher; }
}
}
}