diff --git a/FastCloner.Tests/SpecificScenariosTest.cs b/FastCloner.Tests/SpecificScenariosTest.cs index 7576b2d..4fc4aea 100644 --- a/FastCloner.Tests/SpecificScenariosTest.cs +++ b/FastCloner.Tests/SpecificScenariosTest.cs @@ -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; @@ -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(), "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(), "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() { diff --git a/FastCloner/Code/FastClonerExprGenerator.cs b/FastCloner/Code/FastClonerExprGenerator.cs index 1c1d7c5..af9b700 100644 --- a/FastCloner/Code/FastClonerExprGenerator.cs +++ b/FastCloner/Code/FastClonerExprGenerator.cs @@ -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; @@ -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 KnownTypeProcessors = + new Dictionary + { + [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)) @@ -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>(block, from, state).Compile(); + } + private static object GenerateExpandoObjectProcessor(ExpressionPosition position) { ParameterExpression from = Expression.Parameter(typeof(object));