Skip to content

Commit 0834db3

Browse files
authored
Update extensibility-transforms.md
1 parent eb06dd9 commit 0834db3

File tree

1 file changed

+66
-41
lines changed

1 file changed

+66
-41
lines changed

aspnetcore/fundamentals/servers/yarp/extensibility-transforms.md

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ ms.topic: article
99
content_well_notification: AI-contribution
1010
ai-usage: ai-assisted
1111
---
12-
1312
# Request and Response Transform Extensibility
1413

1514
## Introduction
@@ -23,23 +22,23 @@ All request transforms must derive from the abstract base class [RequestTransfor
2322

2423
A request transform may conditionally produce an immediate response such as for error conditions. This prevents any remaining transforms from running and the request from being proxied. This is indicated by setting the `HttpResponse.StatusCode` to a value other than 200, or calling `HttpResponse.StartAsync()`, or writing to the `HttpResponse.Body` or `BodyWriter`.
2524

26-
### AddRequestTransform
25+
### `AddRequestTransform`
2726

2827
[AddRequestTransform](xref:Yarp.ReverseProxy.Transforms.TransformBuilderContextFuncExtensions.AddRequestTransform*) is a `TransformBuilderContext` extension method that defines a request transform as a `Func<RequestTransformContext, ValueTask>`. This allows creating a custom request transform without implementing a `RequestTransform` derived class.
2928

30-
## ResponseTransform
29+
## `ResponseTransform`
3130

3231
All response transforms must derive from the abstract base class [ResponseTransform](xref:Yarp.ReverseProxy.Transforms.ResponseTransform). These can freely modify the client `HttpResponse`. Avoid reading or modifying the response body as this may disrupt the proxying flow. Consider also adding a parametrized extension method on `TransformBuilderContext` for discoverability and easy of use.
3332

34-
### AddResponseTransform
33+
### `AddResponseTransform`
3534

3635
[AddResponseTransform](xref:Yarp.ReverseProxy.Transforms.TransformBuilderContextFuncExtensions.AddResponseTransform*) is a `TransformBuilderContext` extension method that defines a response transform as a `Func<ResponseTransformContext, ValueTask>`. This allows creating a custom response transform without implementing a `ResponseTransform` derived class.
3736

38-
## ResponseTrailersTransform
37+
## `ResponseTrailersTransform`
3938

4039
All response trailers transforms must derive from the abstract base class [ResponseTrailersTransform](xref:Yarp.ReverseProxy.Transforms.ResponseTrailersTransform). These can freely modify the client HttpResponse trailers. These run after the response body and should not attempt to modify the response headers or body. Consider also adding a parametrized extension method on `TransformBuilderContext` for discoverability and easy of use.
4140

42-
### AddResponseTrailersTransform
41+
### `AddResponseTrailersTransform`
4342

4443
[AddResponseTrailersTransform](xref:Yarp.ReverseProxy.Transforms.TransformBuilderContextFuncExtensions.AddResponseTrailersTransform*) is a `TransformBuilderContext` extension method that defines a response trailers transform as a `Func<ResponseTrailersTransformContext, ValueTask>`. This allows creating a custom response trailers transform without implementing a `ResponseTrailersTransform` derived class.
4544

@@ -53,22 +52,25 @@ The below example uses simple, inefficient buffering to transform requests. A mo
5352

5453
This sample requires YARP 1.1, see https://github.com/microsoft/reverse-proxy/pull/1569.
5554

56-
```C#
55+
```csharp
5756
.AddTransforms(context =>
5857
{
5958
context.AddRequestTransform(async requestContext =>
6059
{
61-
using var reader = new StreamReader(requestContext.HttpContext.Request.Body);
60+
using var reader =
61+
new StreamReader(requestContext.HttpContext.Request.Body);
6262
// TODO: size limits, timeouts
6363
var body = await reader.ReadToEndAsync();
6464
if (!string.IsNullOrEmpty(body))
6565
{
6666
body = body.Replace("Alpha", "Charlie");
6767
var bytes = Encoding.UTF8.GetBytes(body);
68-
// Change Content-Length to match the modified body, or remove it.
68+
// Change Content-Length to match the modified body, or remove it
6969
requestContext.HttpContext.Request.Body = new MemoryStream(bytes);
70-
// Request headers are copied before transforms are invoked, update any needed headers on the ProxyRequest
71-
requestContext.ProxyRequest.Content.Headers.ContentLength = bytes.Length;
70+
// Request headers are copied before transforms are invoked, update any
71+
// needed headers on the ProxyRequest
72+
requestContext.ProxyRequest.Content.Headers.ContentLength =
73+
bytes.Length;
7274
}
7375
});
7476
});
@@ -82,12 +84,13 @@ Be careful about which kinds of responses are modified, how much data gets buffe
8284

8385
The below example uses simple, inefficient buffering to transform responses. A more efficient implementation would wrap the stream returned by `ReadAsStreamAsync()` with a stream that performed the needed modifications as data was proxied from client to server. That would also require removing the Content-Length header since the final length would not be known in advance.
8486

85-
```C#
87+
```csharp
8688
.AddTransforms(context =>
8789
{
8890
context.AddResponseTransform(async responseContext =>
8991
{
90-
var stream = await responseContext.ProxyResponse.Content.ReadAsStreamAsync();
92+
var stream =
93+
await responseContext.ProxyResponse.Content.ReadAsStreamAsync();
9194
using var reader = new StreamReader(stream);
9295
// TODO: size limits, timeouts
9396
var body = await reader.ReadToEndAsync();
@@ -98,77 +101,90 @@ The below example uses simple, inefficient buffering to transform responses. A m
98101

99102
body = body.Replace("Bravo", "Charlie");
100103
var bytes = Encoding.UTF8.GetBytes(body);
101-
// Change Content-Length to match the modified body, or remove it.
104+
// Change Content-Length to match the modified body, or remove it
102105
responseContext.HttpContext.Response.ContentLength = bytes.Length;
103-
// Response headers are copied before transforms are invoked, update any needed headers on the HttpContext.Response.
106+
// Response headers are copied before transforms are invoked, update
107+
// any needed headers on the HttpContext.Response
104108
await responseContext.HttpContext.Response.Body.WriteAsync(bytes);
105109
}
106110
});
107111
});
108112
```
109113

110-
## ITransformProvider
114+
## `ITransformProvider`
111115

112-
[ITransformProvider](xref:Yarp.ReverseProxy.Transforms.Builder.ITransformProvider) provides the functionality of `AddTransforms` described above as well as DI integration and validation support.
116+
<xref:Yarp.ReverseProxy.Transforms.Builder.ITransformProvider> provides the functionality of `AddTransforms` described above as well as DI integration and validation support.
113117

114118
`ITransformProvider`'s can be registered in DI by calling [AddTransforms&lt;T&gt;()](xref:Microsoft.Extensions.DependencyInjection.ReverseProxyServiceCollectionExtensions). Multiple `ITransformProvider` implementations can be registered and all will be run.
115119

116120
`ITransformProvider` has two methods, `Validate` and `Apply`. `Validate` gives you the opportunity to inspect the route for any parameters that are needed to configure a transform, such as custom metadata, and to return validation errors on the context if any needed values are missing or invalid. The `Apply` method provides the same functionality as AddTransform as discussed above, adding and configuring transforms per route.
117121

118-
```C#
122+
```csharp
119123
services.AddReverseProxy()
120124
.LoadFromConfig(_configuration.GetSection("ReverseProxy"))
121125
.AddTransforms<MyTransformProvider>();
122126
```
123-
```C#
127+
128+
```csharp
124129
internal class MyTransformProvider : ITransformProvider
125130
{
126131
public void ValidateRoute(TransformRouteValidationContext context)
127132
{
128-
// Check all routes for a custom property and validate the associated transform data.
129-
if (context.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ?? false)
133+
// Check all routes for a custom property and validate the associated
134+
// transform data
135+
if (context.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ??
136+
false)
130137
{
131138
if (string.IsNullOrEmpty(value))
132139
{
133-
context.Errors.Add(new ArgumentException("A non-empty CustomMetadata value is required"));
140+
context.Errors.Add(new ArgumentException(
141+
"A non-empty CustomMetadata value is required"));
134142
}
135143
}
136144
}
137145

138146
public void ValidateCluster(TransformClusterValidationContext context)
139147
{
140-
// Check all clusters for a custom property and validate the associated transform data.
141-
if (context.Cluster.Metadata?.TryGetValue("CustomMetadata", out var value) ?? false)
148+
// Check all clusters for a custom property and validate the associated
149+
// transform data.
150+
if (context.Cluster.Metadata?.TryGetValue("CustomMetadata", out var value)
151+
?? false)
142152
{
143153
if (string.IsNullOrEmpty(value))
144154
{
145-
context.Errors.Add(new ArgumentException("A non-empty CustomMetadata value is required"));
155+
context.Errors.Add(new ArgumentException(
156+
"A non-empty CustomMetadata value is required"));
146157
}
147158
}
148159
}
149160

150161
public void Apply(TransformBuilderContext transformBuildContext)
151162
{
152163
// Check all routes for a custom property and add the associated transform.
153-
if ((transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ?? false)
154-
|| (transformBuildContext.Cluster?.Metadata?.TryGetValue("CustomMetadata", out value) ?? false))
164+
if ((transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata",
165+
out var value) ?? false)
166+
|| (transformBuildContext.Cluster?.Metadata?.TryGetValue(
167+
"CustomMetadata", out value) ?? false))
155168
{
156169
if (string.IsNullOrEmpty(value))
157170
{
158-
throw new ArgumentException("A non-empty CustomMetadata value is required");
171+
throw new ArgumentException(
172+
"A non-empty CustomMetadata value is required");
159173
}
160174

161175
transformBuildContext.AddRequestTransform(transformContext =>
162176
{
163-
transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey<string>("CustomMetadata"), value);
177+
transformContext.ProxyRequest.Options.Set(
178+
new HttpRequestOptionsKey<string>("CustomMetadata"), value);
179+
164180
return default;
165181
});
166182
}
167183
}
168184
}
169185
```
170186

171-
## ITransformFactory
187+
## `ITransformFactory`
172188

173189
Developers that want to integrate their custom transforms with the `Transforms` section of configuration can implement an [ITransformFactory](xref:Yarp.ReverseProxy.Transforms.Builder.ITransformFactory). This should be registered in DI using the `AddTransformFactory<T>()` method. Multiple factories can be registered and all will be used.
174190

@@ -178,40 +194,47 @@ The `Validate` method is called when loading a configuration to verify the conte
178194

179195
The `Build` method takes the given configuration and produces the associated transform instances for the route.
180196

181-
```C#
197+
```csharp
182198
services.AddReverseProxy()
183199
.LoadFromConfig(_configuration.GetSection("ReverseProxy"))
184200
.AddTransformFactory<MyTransformFactory>();
185201
```
186-
```C#
202+
203+
```csharp
187204
internal class MyTransformFactory : ITransformFactory
188205
{
189-
public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)
206+
public bool Validate(TransformRouteValidationContext context,
207+
IReadOnlyDictionary<string, string> transformValues)
190208
{
191209
if (transformValues.TryGetValue("CustomTransform", out var value))
192210
{
193211
if (string.IsNullOrEmpty(value))
194212
{
195-
context.Errors.Add(new ArgumentException("A non-empty CustomTransform value is required"));
213+
context.Errors.Add(new ArgumentException(
214+
"A non-empty CustomTransform value is required"));
196215
}
197216

198217
return true; // Matched
199218
}
219+
200220
return false;
201221
}
202222

203-
public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)
223+
public bool Build(TransformBuilderContext context,
224+
IReadOnlyDictionary<string, string> transformValues)
204225
{
205226
if (transformValues.TryGetValue("CustomTransform", out var value))
206227
{
207228
if (string.IsNullOrEmpty(value))
208229
{
209-
throw new ArgumentException("A non-empty CustomTransform value is required");
230+
throw new ArgumentException(
231+
"A non-empty CustomTransform value is required");
210232
}
211233

212234
context.AddRequestTransform(transformContext =>
213235
{
214-
transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey<string>("CustomTransform"), value);
236+
transformContext.ProxyRequest.Options.Set(
237+
new HttpRequestOptionsKey<string>("CustomTransform"), value);
215238
return default;
216239
});
217240

@@ -227,14 +250,16 @@ internal class MyTransformFactory : ITransformFactory
227250

228251
Consider also adding parametrized extension methods on `RouteConfig` like `WithTransformQueryValue` to facilitate programmatic route construction.
229252

230-
```C#
231-
public static RouteConfig WithTransformQueryValue(this RouteConfig routeConfig, string queryKey, string value, bool append = true)
253+
```csharp
254+
public static RouteConfig WithTransformQueryValue(this RouteConfig routeConfig,
255+
string queryKey, string value, bool append = true)
232256
{
233-
var type = append ? QueryTransformFactory.AppendKey : QueryTransformFactory.SetKey;
257+
var type = append ? QueryTransformFactory.AppendKey :
258+
QueryTransformFactory.SetKey;
234259
return routeConfig.WithTransform(transform =>
235260
{
236261
transform[QueryTransformFactory.QueryValueParameterKey] = queryKey;
237262
transform[type] = value;
238263
});
239264
}
240-
```
265+
```

0 commit comments

Comments
 (0)