-
Notifications
You must be signed in to change notification settings - Fork 34
/
RouteLinker.cs
255 lines (234 loc) · 10.6 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
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;
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, IDisposable
{
private readonly HttpRequestMessage request;
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)" />
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="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)" />
public RouteLinker(HttpRequestMessage request, IRouteDispatcher dispatcher)
{
if (request == null)
throw new ArgumentNullException("request");
if (dispatcher == null)
throw new ArgumentNullException("dispatcher");
this.request = request;
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>
/// <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 apporpriate 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 a 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)
{
if (method == null)
throw new ArgumentNullException("method");
var methodCallExp = method.Body as MethodCallExpression;
if (methodCallExp == null)
throw new ArgumentException("The expression's body must be a MethodCallExpression. The code block supplied should invoke a method.\nExample: x => x.Foo().", "method");
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 = methodCallExp.Method.GetParameters()
.ToDictionary(p => p.Name, p => GetValue(methodCallExp, p));
return this.dispatcher.Dispatch(methodCallExp.Method, routeValues);
}
private static object GetValue(MethodCallExpression methodCallExp,
ParameterInfo p)
{
var arg = methodCallExp.Arguments[p.Position];
var lambda = Expression.Lambda(arg);
return lambda.Compile().DynamicInvoke().ToString();
}
private Uri GetRelativeUri(Rouple r)
{
var urlHelper = this.CreateUrlHelper();
var relativeUri = urlHelper.Route(r.RouteName, r.RouteValues);
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 r = new HttpRequestMessage(
this.request.Method,
this.request.RequestUri);
try
{
foreach (var kvp in this.request.Properties)
if (kvp.Key != HttpPropertyKeys.HttpRouteDataKey)
r.Properties.Add(kvp.Key, kvp.Value);
var routeData = this.request.GetRouteData();
r.Properties.Add(
HttpPropertyKeys.HttpRouteDataKey,
new HttpRouteData(routeData.Route));
return r;
}
catch
{
r.Dispose();
throw;
}
}
/// <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 dispatcher.
/// </summary>
/// <seealso cref="RouteLinker(HttpRequestMessage, IRouteDispatcher)" />
public IRouteDispatcher RouteDispatcher
{
get { return this.dispatcher; }
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting
/// unmanaged resources.
/// </summary>
/// <remarks>
/// <para>
/// This implementation follows the standard Dispose pattern by delegating to the virtual
/// <see cref="Dispose(bool)" /> method.
/// </para>
/// </remarks>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
/// <remarks>
/// <para>
/// If <paramref name="disposing" /> is <see langword="true" /> this method disposes
/// <see cref="Request" />.
/// </para>
/// </remarks>
protected virtual void Dispose(bool disposing)
{
if (disposing)
this.request.Dispose();
}
}
}