Skip to content

Commit 9a39d4f

Browse files
committed
support HttpRequestOptions
1 parent 7b1cdac commit 9a39d4f

File tree

2 files changed

+155
-2
lines changed

2 files changed

+155
-2
lines changed

FastCloner.Tests/SpecificScenariosTest.cs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
using System.Drawing;
88
using System.Dynamic;
99
using System.Globalization;
10+
using System.Net;
11+
using System.Net.Http.Headers;
12+
using System.Text;
1013
using Microsoft.EntityFrameworkCore;
1114

1215
namespace FastCloner.Tests;
@@ -304,6 +307,118 @@ public void Dynamic_With_Collection_Clone()
304307
});
305308
}
306309

310+
[Test]
311+
public void HttpRequest_Clone()
312+
{
313+
// Arrange
314+
HttpRequestMessage original = new HttpRequestMessage
315+
{
316+
Method = HttpMethod.Post,
317+
RequestUri = new Uri("https://api.example.com/data"),
318+
Version = new Version(2, 0),
319+
Content = new StringContent(
320+
"{\"key\":\"value\"}",
321+
Encoding.UTF8,
322+
"application/json")
323+
};
324+
325+
original.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
326+
original.Headers.Add("Custom-Header", "test-value");
327+
original.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "test-token");
328+
329+
// Act
330+
HttpRequestMessage? cloned = FastCloner.DeepClone(original);
331+
332+
// Assert
333+
Assert.Multiple(() =>
334+
{
335+
Assert.That(cloned.Method, Is.EqualTo(HttpMethod.Post), "Method should be copied");
336+
Assert.That(cloned.RequestUri?.ToString(), Is.EqualTo("https://api.example.com/data"), "URI should be copied");
337+
Assert.That(cloned.Version, Is.EqualTo(new Version(2, 0)), "Version should be copied");
338+
339+
Assert.That(cloned.Headers.Accept.First().MediaType, Is.EqualTo("application/json"), "Accept header should be copied");
340+
Assert.That(cloned.Headers.GetValues("Custom-Header").First(), Is.EqualTo("test-value"), "Custom header should be copied");
341+
Assert.That(cloned.Headers.Authorization?.Scheme, Is.EqualTo("Bearer"), "Authorization scheme should be copied");
342+
Assert.That(cloned.Headers.Authorization?.Parameter, Is.EqualTo("test-token"), "Authorization parameter should be copied");
343+
344+
Assert.That(cloned.Content, Is.Not.Null, "Content should be cloned");
345+
Assert.That(cloned.Content, Is.TypeOf<StringContent>(), "Content type should be preserved");
346+
347+
string originalContent = original.Content.ReadAsStringAsync().Result;
348+
string clonedContent = cloned.Content.ReadAsStringAsync().Result;
349+
Assert.That(clonedContent, Is.EqualTo(originalContent), "Content value should be copied");
350+
Assert.That(cloned.Content.Headers.ContentType?.MediaType, Is.EqualTo("application/json"), "Content-Type should be copied");
351+
});
352+
}
353+
354+
[Test]
355+
public void HttpRequest_With_MultipartContent_Clone()
356+
{
357+
// Arrange
358+
HttpRequestMessage original = new HttpRequestMessage
359+
{
360+
Method = HttpMethod.Post,
361+
RequestUri = new Uri("https://api.example.com/upload")
362+
};
363+
364+
MultipartFormDataContent multipartContent = new MultipartFormDataContent();
365+
366+
StringContent stringContent = new StringContent("text data", Encoding.UTF8);
367+
multipartContent.Add(stringContent, "text");
368+
369+
byte[] binaryData = "binary data"u8.ToArray();
370+
ByteArrayContent byteContent = new ByteArrayContent(binaryData);
371+
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
372+
multipartContent.Add(byteContent, "file", "test.bin");
373+
374+
original.Content = multipartContent;
375+
376+
// Act
377+
HttpRequestMessage? cloned = FastCloner.DeepClone(original);
378+
379+
// Assert
380+
Assert.Multiple(() =>
381+
{
382+
Assert.That(cloned.Content, Is.TypeOf<MultipartFormDataContent>(), "Content type should be preserved");
383+
384+
MultipartFormDataContent? originalMultipart = (MultipartFormDataContent)original.Content;
385+
MultipartFormDataContent? clonedMultipart = (MultipartFormDataContent)cloned.Content;
386+
387+
string originalParts = originalMultipart.ReadAsStringAsync().Result;
388+
string clonedParts = clonedMultipart.ReadAsStringAsync().Result;
389+
390+
Assert.That(clonedParts, Is.EqualTo(originalParts), "Multipart content should be identical");
391+
Assert.That(clonedMultipart.Headers.ContentType?.Parameters.First(p => p.Name == "boundary").Value, Is.Not.Null, "Boundary should be present");
392+
});
393+
}
394+
395+
[Test]
396+
public void HttpRequest_With_Handlers_Clone()
397+
{
398+
// Arrange
399+
HttpRequestMessage original = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com");
400+
HttpClientHandler handler = new HttpClientHandler
401+
{
402+
AllowAutoRedirect = false,
403+
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
404+
UseCookies = false
405+
};
406+
407+
original.Properties.Add("AllowAutoRedirect", handler.AllowAutoRedirect);
408+
original.Properties.Add("AutomaticDecompression", handler.AutomaticDecompression);
409+
original.Properties.Add("UseCookies", handler.UseCookies);
410+
411+
HttpRequestMessage? cloned = FastCloner.DeepClone(original);
412+
413+
Assert.Multiple(() =>
414+
{
415+
Assert.That(cloned.Properties, Is.Not.Empty, "Properties should be copied");
416+
Assert.That(cloned.Properties["AllowAutoRedirect"], Is.EqualTo(false), "Handler property should be copied");
417+
Assert.That(cloned.Properties["AutomaticDecompression"], Is.EqualTo(DecompressionMethods.GZip | DecompressionMethods.Deflate), "Handler compression settings should be copied");
418+
Assert.That(cloned.Properties["UseCookies"], Is.EqualTo(false), "Handler cookie settings should be copied");
419+
});
420+
}
421+
307422
[Test]
308423
public void Dynamic_With_Dictionary_Clone()
309424
{

FastCloner/Code/FastClonerExprGenerator.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections;
22
using System.Collections.Concurrent;
3+
using System.Collections.Frozen;
34
using System.Dynamic;
45
using System.Linq.Expressions;
56
using System.Reflection;
@@ -54,11 +55,21 @@ private static LabelTarget CreateLoopLabel(ExpressionPosition position)
5455
public static bool IsSetType(Type type) => type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>));
5556
private static bool IsDictionaryType(Type type) => typeof(IDictionary).IsAssignableFrom(type) || type.GetInterfaces().Any(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>) || i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)));
5657

58+
private delegate object ProcessMethodDelegate(Type type, bool unboxStruct, ExpressionPosition position);
59+
60+
private static readonly FrozenDictionary<Type, ProcessMethodDelegate> KnownTypeProcessors =
61+
new Dictionary<Type, ProcessMethodDelegate>
62+
{
63+
[typeof(ExpandoObject)] = (_, _, position) => GenerateExpandoObjectProcessor(position),
64+
[typeof(HttpRequestOptions)] = (_, _, position) => GenerateHttpRequestOptionsProcessor(position),
65+
[typeof(Array)] = (type, _, _) => GenerateProcessArrayMethod(type)
66+
}.ToFrozenDictionary();
67+
5768
private static object GenerateProcessMethod(Type type, bool unboxStruct, ExpressionPosition position)
5869
{
59-
if (type == typeof(ExpandoObject))
70+
if (KnownTypeProcessors.TryGetValue(type, out ProcessMethodDelegate? handler))
6071
{
61-
return GenerateExpandoObjectProcessor(position);
72+
return handler.Invoke(type, unboxStruct, position);
6273
}
6374

6475
if (IsDictionaryType(type))
@@ -194,6 +205,33 @@ private static object GenerateProcessMethod(Type type, bool unboxStruct, Express
194205
return Expression.Lambda(funcType, Expression.Block(blockParams, expressionList), from, state).Compile();
195206
}
196207

208+
private static object GenerateHttpRequestOptionsProcessor(ExpressionPosition position)
209+
{
210+
ParameterExpression from = Expression.Parameter(typeof(object));
211+
ParameterExpression state = Expression.Parameter(typeof(FastCloneState));
212+
ParameterExpression result = Expression.Variable(typeof(HttpRequestOptions));
213+
ParameterExpression tempMessage = Expression.Variable(typeof(HttpRequestMessage));
214+
ParameterExpression fromOptions = Expression.Variable(typeof(HttpRequestOptions));
215+
216+
ConstructorInfo constructor = typeof(HttpRequestMessage).GetConstructor(Type.EmptyTypes)!;
217+
218+
BlockExpression block = Expression.Block(
219+
[result, tempMessage, fromOptions],
220+
Expression.Assign(fromOptions, Expression.Convert(from, typeof(HttpRequestOptions))),
221+
Expression.Assign(tempMessage, Expression.New(constructor)),
222+
Expression.Assign(result, Expression.Property(tempMessage, "Options")),
223+
Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, result),
224+
Expression.Assign(result, Expression.Convert(
225+
Expression.Call(fromOptions, typeof(object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance)!),
226+
typeof(HttpRequestOptions)
227+
)),
228+
Expression.Call(tempMessage, typeof(IDisposable).GetMethod("Dispose")!),
229+
result
230+
);
231+
232+
return Expression.Lambda<Func<object, FastCloneState, object>>(block, from, state).Compile();
233+
}
234+
197235
private static object GenerateExpandoObjectProcessor(ExpressionPosition position)
198236
{
199237
ParameterExpression from = Expression.Parameter(typeof(object));

0 commit comments

Comments
 (0)