Skip to content

Commit

Permalink
support HttpRequestOptions
Browse files Browse the repository at this point in the history
  • Loading branch information
lofcz committed Jan 8, 2025
1 parent 7b1cdac commit 9a39d4f
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 2 deletions.
115 changes: 115 additions & 0 deletions FastCloner.Tests/SpecificScenariosTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
using System.Drawing;
using System.Dynamic;
using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using Microsoft.EntityFrameworkCore;

namespace FastCloner.Tests;
Expand Down Expand Up @@ -304,6 +307,118 @@ public void Dynamic_With_Collection_Clone()
});
}

[Test]
public void HttpRequest_Clone()
{
// Arrange
HttpRequestMessage original = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://api.example.com/data"),
Version = new Version(2, 0),
Content = new StringContent(
"{\"key\":\"value\"}",
Encoding.UTF8,
"application/json")
};

original.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
original.Headers.Add("Custom-Header", "test-value");
original.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "test-token");

// Act
HttpRequestMessage? cloned = FastCloner.DeepClone(original);

// Assert
Assert.Multiple(() =>
{
Assert.That(cloned.Method, Is.EqualTo(HttpMethod.Post), "Method should be copied");
Assert.That(cloned.RequestUri?.ToString(), Is.EqualTo("https://api.example.com/data"), "URI should be copied");
Assert.That(cloned.Version, Is.EqualTo(new Version(2, 0)), "Version should be copied");

Assert.That(cloned.Headers.Accept.First().MediaType, Is.EqualTo("application/json"), "Accept header should be copied");
Assert.That(cloned.Headers.GetValues("Custom-Header").First(), Is.EqualTo("test-value"), "Custom header should be copied");
Assert.That(cloned.Headers.Authorization?.Scheme, Is.EqualTo("Bearer"), "Authorization scheme should be copied");
Assert.That(cloned.Headers.Authorization?.Parameter, Is.EqualTo("test-token"), "Authorization parameter should be copied");

Assert.That(cloned.Content, Is.Not.Null, "Content should be cloned");
Assert.That(cloned.Content, Is.TypeOf<StringContent>(), "Content type should be preserved");

string originalContent = original.Content.ReadAsStringAsync().Result;
string clonedContent = cloned.Content.ReadAsStringAsync().Result;
Assert.That(clonedContent, Is.EqualTo(originalContent), "Content value should be copied");
Assert.That(cloned.Content.Headers.ContentType?.MediaType, Is.EqualTo("application/json"), "Content-Type should be copied");
});
}

[Test]
public void HttpRequest_With_MultipartContent_Clone()
{
// Arrange
HttpRequestMessage original = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://api.example.com/upload")
};

MultipartFormDataContent multipartContent = new MultipartFormDataContent();

StringContent stringContent = new StringContent("text data", Encoding.UTF8);
multipartContent.Add(stringContent, "text");

byte[] binaryData = "binary data"u8.ToArray();
ByteArrayContent byteContent = new ByteArrayContent(binaryData);
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
multipartContent.Add(byteContent, "file", "test.bin");

original.Content = multipartContent;

// Act
HttpRequestMessage? cloned = FastCloner.DeepClone(original);

// Assert
Assert.Multiple(() =>
{
Assert.That(cloned.Content, Is.TypeOf<MultipartFormDataContent>(), "Content type should be preserved");

MultipartFormDataContent? originalMultipart = (MultipartFormDataContent)original.Content;
MultipartFormDataContent? clonedMultipart = (MultipartFormDataContent)cloned.Content;

string originalParts = originalMultipart.ReadAsStringAsync().Result;
string clonedParts = clonedMultipart.ReadAsStringAsync().Result;

Assert.That(clonedParts, Is.EqualTo(originalParts), "Multipart content should be identical");
Assert.That(clonedMultipart.Headers.ContentType?.Parameters.First(p => p.Name == "boundary").Value, Is.Not.Null, "Boundary should be present");
});
}

[Test]
public void HttpRequest_With_Handlers_Clone()
{
// Arrange
HttpRequestMessage original = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com");
HttpClientHandler handler = new HttpClientHandler
{
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = false
};

original.Properties.Add("AllowAutoRedirect", handler.AllowAutoRedirect);
original.Properties.Add("AutomaticDecompression", handler.AutomaticDecompression);
original.Properties.Add("UseCookies", handler.UseCookies);

HttpRequestMessage? cloned = FastCloner.DeepClone(original);

Assert.Multiple(() =>
{
Assert.That(cloned.Properties, Is.Not.Empty, "Properties should be copied");
Assert.That(cloned.Properties["AllowAutoRedirect"], Is.EqualTo(false), "Handler property should be copied");
Assert.That(cloned.Properties["AutomaticDecompression"], Is.EqualTo(DecompressionMethods.GZip | DecompressionMethods.Deflate), "Handler compression settings should be copied");
Assert.That(cloned.Properties["UseCookies"], Is.EqualTo(false), "Handler cookie settings should be copied");
});
}

[Test]
public void Dynamic_With_Dictionary_Clone()
{
Expand Down
42 changes: 40 additions & 2 deletions FastCloner/Code/FastClonerExprGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Dynamic;
using System.Linq.Expressions;
using System.Reflection;
Expand Down Expand Up @@ -54,11 +55,21 @@ private static LabelTarget CreateLoopLabel(ExpressionPosition position)
public static bool IsSetType(Type type) => type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>));
private static bool IsDictionaryType(Type type) => typeof(IDictionary).IsAssignableFrom(type) || type.GetInterfaces().Any(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>) || i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)));

private delegate object ProcessMethodDelegate(Type type, bool unboxStruct, ExpressionPosition position);

private static readonly FrozenDictionary<Type, ProcessMethodDelegate> KnownTypeProcessors =
new Dictionary<Type, ProcessMethodDelegate>
{
[typeof(ExpandoObject)] = (_, _, position) => GenerateExpandoObjectProcessor(position),
[typeof(HttpRequestOptions)] = (_, _, position) => GenerateHttpRequestOptionsProcessor(position),
[typeof(Array)] = (type, _, _) => GenerateProcessArrayMethod(type)
}.ToFrozenDictionary();

private static object GenerateProcessMethod(Type type, bool unboxStruct, ExpressionPosition position)
{
if (type == typeof(ExpandoObject))
if (KnownTypeProcessors.TryGetValue(type, out ProcessMethodDelegate? handler))
{
return GenerateExpandoObjectProcessor(position);
return handler.Invoke(type, unboxStruct, position);
}

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

private static object GenerateHttpRequestOptionsProcessor(ExpressionPosition position)
{
ParameterExpression from = Expression.Parameter(typeof(object));
ParameterExpression state = Expression.Parameter(typeof(FastCloneState));
ParameterExpression result = Expression.Variable(typeof(HttpRequestOptions));
ParameterExpression tempMessage = Expression.Variable(typeof(HttpRequestMessage));
ParameterExpression fromOptions = Expression.Variable(typeof(HttpRequestOptions));

ConstructorInfo constructor = typeof(HttpRequestMessage).GetConstructor(Type.EmptyTypes)!;

BlockExpression block = Expression.Block(
[result, tempMessage, fromOptions],
Expression.Assign(fromOptions, Expression.Convert(from, typeof(HttpRequestOptions))),
Expression.Assign(tempMessage, Expression.New(constructor)),
Expression.Assign(result, Expression.Property(tempMessage, "Options")),
Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, result),
Expression.Assign(result, Expression.Convert(
Expression.Call(fromOptions, typeof(object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance)!),
typeof(HttpRequestOptions)
)),
Expression.Call(tempMessage, typeof(IDisposable).GetMethod("Dispose")!),
result
);

return Expression.Lambda<Func<object, FastCloneState, object>>(block, from, state).Compile();
}

private static object GenerateExpandoObjectProcessor(ExpressionPosition position)
{
ParameterExpression from = Expression.Parameter(typeof(object));
Expand Down

0 comments on commit 9a39d4f

Please sign in to comment.