From d9b3e0a554f3c1a9eeaab54f48487990f374a2fc Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Sat, 12 Nov 2022 18:39:20 -0300 Subject: [PATCH 01/12] Add alternate extension method for using an object as request parameters when the type is known at compile time --- .../Extensions/ReflectionExtensions.cs | 8 +- src/RestSharp/Parameters/ObjectParser.cs | 10 +- ...RestRequestExtensions.ParameterProperty.cs | 21 ++ .../RestRequestExtensions.TypeCache.cs | 220 ++++++++++++++++++ .../Request/RestRequestExtensions.cs | 41 ++-- ...ParametersShouldBeStringified.ITestCase.cs | 24 ++ ...ametersShouldBeStringified.NoAttributes.cs | 24 ++ ...etersShouldBeStringified.WithAttributes.cs | 24 ++ ...eterTests.ParametersShouldBeStringified.cs | 112 +++++++++ test/RestSharp.Tests/ObjectParameterTests.cs | 15 +- test/RestSharp.Tests/SampleData/TestData.cs | 9 + 11 files changed, 481 insertions(+), 27 deletions(-) create mode 100644 src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs create mode 100644 src/RestSharp/Request/RestRequestExtensions.TypeCache.cs create mode 100644 test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.ITestCase.cs create mode 100644 test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.NoAttributes.cs create mode 100644 test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.WithAttributes.cs create mode 100644 test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.cs create mode 100644 test/RestSharp.Tests/SampleData/TestData.cs diff --git a/src/RestSharp/Extensions/ReflectionExtensions.cs b/src/RestSharp/Extensions/ReflectionExtensions.cs index a16513535..5977de18a 100644 --- a/src/RestSharp/Extensions/ReflectionExtensions.cs +++ b/src/RestSharp/Extensions/ReflectionExtensions.cs @@ -1,11 +1,11 @@ // Copyright (c) .NET Foundation and Contributors -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,7 +15,7 @@ using System.Globalization; using System.Reflection; -namespace RestSharp.Extensions; +namespace RestSharp.Extensions; /// /// Reflection extensions diff --git a/src/RestSharp/Parameters/ObjectParser.cs b/src/RestSharp/Parameters/ObjectParser.cs index 71e496798..52ee7e8cf 100644 --- a/src/RestSharp/Parameters/ObjectParser.cs +++ b/src/RestSharp/Parameters/ObjectParser.cs @@ -1,17 +1,17 @@ // Copyright (c) .NET Foundation and Contributors -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// +// using System.Reflection; @@ -58,7 +58,7 @@ static class ObjectParser { RequestArrayQueryType.ArrayParameters => values.Select(x => ($"{name}[]", x)), _ => throw new ArgumentOutOfRangeException() }; - + } return new (string, string?)[] { (name, null) }; diff --git a/src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs b/src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs new file mode 100644 index 000000000..6899db28a --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Reflection; + +namespace RestSharp; + +public static partial class RestRequestExtensions { + sealed record ParameterProperty(string Name, IEnumerable Values); +} diff --git a/src/RestSharp/Request/RestRequestExtensions.TypeCache.cs b/src/RestSharp/Request/RestRequestExtensions.TypeCache.cs new file mode 100644 index 000000000..805037bcb --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.TypeCache.cs @@ -0,0 +1,220 @@ +// Copyright (c) .NET Foundation and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using RestSharp.Extensions; +using System.Collections; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace RestSharp; + +public static partial class RestRequestExtensions { + static partial class TypeCache where T : class { +#if NET5_0_OR_GREATER + const char CsvSeparator = ','; +#else + const string CsvSeparator = ","; +#endif + const string ArrayBrackets = "[]"; + + static readonly IReadOnlyDictionary> NameToGetter = + typeof(T) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(property => { + var requestProperty = + property.GetAttribute() ?? + new RequestPropertyAttribute(); + + return new { + property.Name, + // The getter can only be null if it is non-public, + // but we are already filtering out non-public properties + // through the binding flags, so it's fine. + GetMethod = property.GetGetMethod()!, + RequestProperty = requestProperty + }; + }) +#if NET5_0_OR_GREATER + // We must filter out ref structs otherwise they will + // break LINQ expressions apart once we compile them. + .Where(property => !property.GetMethod.ReturnType.IsByRefLike) +#endif + .ToDictionary( + property => property.Name, + property => CompileGetter( + property.GetMethod, + property.RequestProperty.Name ?? property.Name, + property.RequestProperty)); + + internal static IEnumerable GetParameters(T obj, params string[] includedProperties) => + includedProperties.Length == 0 ? + GetParameters(obj) : + GetParameters(obj, includedProperties.Select(property => NameToGetter[property])); + + internal static IEnumerable GetParameters(T obj) => + GetParameters(obj, NameToGetter.Values); + + static IEnumerable GetParameters(T obj, IEnumerable> getters) => + getters + .Select(getParameter => getParameter(obj)) + .SelectMany(property => property.Values.Select(value => new GetOrPostParameter(property.Name, value))); + + static Func CompileGetter(MethodInfo getMethod, string propertyName, RequestPropertyAttribute requestProperty) { + var modelParameter = Expression.Parameter(typeof(T)); + + var callGetter = Expression.Call(modelParameter, getMethod); + + return getMethod.ReturnType switch { + var @return when typeof(IFormattable).IsAssignableFrom(@return) => + @return.IsValueType ? + GetFormattableGetter(modelParameter, Expression.Convert(callGetter, typeof(IFormattable)), propertyName, requestProperty) : + GetFormattableGetter(modelParameter, callGetter, propertyName, requestProperty), + var @return when typeof(IConvertible).IsAssignableFrom(@return) => + @return.IsValueType ? + GetConvertibleGetter(modelParameter, Expression.Convert(callGetter, typeof(IConvertible)), propertyName) : + GetConvertibleGetter(modelParameter, callGetter, propertyName), + var @return when typeof(IEnumerable).IsAssignableFrom(@return) => + @return.IsValueType ? + GetEnumerableGetter(modelParameter, Expression.Convert(callGetter, typeof(IEnumerable)), propertyName, requestProperty, @return) : + GetEnumerableGetter(modelParameter, callGetter, propertyName, requestProperty, @return), + var @return => + @return.IsValueType ? + GetObjectGetter(modelParameter, Expression.Convert(callGetter, typeof(object)), propertyName, requestProperty, @return) : + GetObjectGetter(modelParameter, callGetter, propertyName, requestProperty, @return) + }; + } + + static Func GetFormattableGetter(ParameterExpression modelParameter, Expression callGetter, string propertyName, RequestPropertyAttribute requestProperty) { + var getFormattable = Expression.Lambda>(callGetter, modelParameter).Compile(); + + return model => new ParameterProperty(propertyName, new[] { + getFormattable(model).ToString(requestProperty.Format, null) + }); + } + + static Func GetConvertibleGetter(ParameterExpression modelParameter, Expression callGetter, string propertyName) { + var getFormattable = Expression.Lambda>(callGetter, modelParameter).Compile(); + + return model => new ParameterProperty(propertyName, new[] { + getFormattable(model).ToString(null) + }); + } + + static Func GetEnumerableGetter(ParameterExpression modelParameter, Expression callGetter, string propertyName, RequestPropertyAttribute requestProperty, Type @return) { + + var getEnumerable = Expression.Lambda>(callGetter, modelParameter).Compile(); + + var getValues = GetGetFormattedStringValues(getEnumerable, requestProperty, @return); + + var actualPropertyName = + requestProperty.ArrayQueryType == RequestArrayQueryType.ArrayParameters ? + $"{propertyName}{ArrayBrackets}" : + propertyName; + + return model => new ParameterProperty(actualPropertyName, getValues(model)); + } + + static Func> GetGetFormattedStringValues(Func getEnumerable, RequestPropertyAttribute requestProperty, Type @return) { + + var getStringValues = GetGetRawStringValues(getEnumerable, requestProperty, @return); + + return requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => model => new[] { string.Join(CsvSeparator, getStringValues(model)) }, + RequestArrayQueryType.ArrayParameters => getStringValues, + _ => model => Enumerable.Empty() + }; + } + + static Func> GetGetRawStringValues(Func getEnumerable, RequestPropertyAttribute requestProperty, Type @return) { + var enumerableInterfaces = + @return + .GetInterfaces() + .Append(@return) // In case `@return` is `IEnumerable<>`: https://stackoverflow.com/a/63794667 + .Where( + @interface => + @interface.IsGenericType && + @interface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + .ToArray(); + + if (enumerableInterfaces.Length != 1) { + return requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => model => getEnumerable(model).Cast().Select(value => string.Join(CsvSeparator, GetStringValues(value, requestProperty))), + RequestArrayQueryType.ArrayParameters => model => getEnumerable(model).Cast().SelectMany(value => GetStringValues(value, requestProperty)), + _ => model => Enumerable.Empty() + }; + } + + return enumerableInterfaces[0].GetGenericArguments()[0] switch { + var enumerated when typeof(IFormattable).IsAssignableFrom(enumerated) => + enumerated.IsValueType ? + model => getEnumerable(model).Cast().Select(formattable => formattable.ToString(requestProperty.Format, null)) : + model => Unsafe.As>(getEnumerable(model)).Select(formattable => formattable.ToString(requestProperty.Format, null)), + var enumerated when typeof(IConvertible).IsAssignableFrom(enumerated) => + enumerated.IsValueType ? + model => getEnumerable(model).Cast().Select(formattable => formattable.ToString(null)) : + model => Unsafe.As>(getEnumerable(model)).Select(formattable => formattable.ToString(null)), + + var enumerated => + requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => + enumerated.IsValueType ? + model => new[] { string.Join(CsvSeparator, getEnumerable(model).Cast().Select(value => string.Join(CsvSeparator, GetStringValues(value, requestProperty)))) } : + model => new[] { string.Join(CsvSeparator, Unsafe.As>(getEnumerable(model)).Select(value => string.Join(CsvSeparator, GetStringValues(value, requestProperty)))) }, + RequestArrayQueryType.ArrayParameters => + enumerated.IsValueType ? + model => getEnumerable(model).Cast().SelectMany(value => GetStringValues(value, requestProperty)) : + model => Unsafe.As>(getEnumerable(model)).SelectMany(value => GetStringValues(value, requestProperty)), + _ => model => Enumerable.Empty() + } + }; + } + + static Func GetObjectGetter(ParameterExpression modelParameter, Expression callGetter, string propertyName, RequestPropertyAttribute requestProperty, Type @return) { + var getObject = Expression.Lambda>(callGetter, modelParameter).Compile(); + + Func getPropertyName = + requestProperty.ArrayQueryType == RequestArrayQueryType.ArrayParameters ? + @object => @object is IEnumerable ? $"{propertyName}{ArrayBrackets}" : propertyName : + _ => propertyName; + + if (@return == typeof(object)) { + return model => { + var @object = getObject(model); + + var values = GetStringValues(@object, requestProperty); + + return new ParameterProperty(getPropertyName(@object), values); + }; + } + + var getterConverter = TypeDescriptor.GetConverter(@return); + return model => new ParameterProperty(propertyName, new[] { + getterConverter.ConvertToString(getObject(model)) + }); + } + + static IEnumerable GetStringValues(object @object, RequestPropertyAttribute requestProperty) => @object switch { + IFormattable formattable => new[] { formattable.ToString(requestProperty.Format, null) }, + IConvertible convertible => new[] { convertible.ToString(null) }, + IEnumerable enumerable => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => new[] { string.Join(CsvSeparator, enumerable.Cast().Select(value => string.Join(CsvSeparator, GetStringValues(value, requestProperty)))) }, + RequestArrayQueryType.ArrayParameters => enumerable.Cast().SelectMany(value => GetStringValues(value, requestProperty)), + _ => Enumerable.Empty() + }, + _ => new[] { TypeDescriptor.GetConverter(@object).ConvertToString(@object) } + }; + } +} diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index a35da5621..80bee3c2c 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -1,11 +1,11 @@ // Copyright (c) .NET Foundation and Contributors -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,7 +20,7 @@ namespace RestSharp; [PublicAPI] -public static class RestRequestExtensions { +public static partial class RestRequestExtensions { static readonly Regex PortSplitRegex = new(@":\d+"); /// @@ -45,6 +45,11 @@ public static RestRequest AddParameter(this RestRequest request, string name, st public static RestRequest AddParameter(this RestRequest request, string name, T value, bool encode = true) where T : struct => request.AddParameter(name, value.ToString(), encode); + static RestRequest AddParameters(this RestRequest request, IEnumerable parameters) { + request.Parameters.AddParameters(parameters); + return request; + } + /// /// Adds or updates a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) /// @@ -330,7 +335,7 @@ public static RestRequest AddFile( => request.AddFile(FileParameter.Create(name, bytes, filename, contentType, options)); /// - /// Adds a file attachment to the request, where the file content will be retrieved from a given stream + /// Adds a file attachment to the request, where the file content will be retrieved from a given stream /// /// Request instance /// Parameter name @@ -446,16 +451,22 @@ public static RestRequest AddObject(this RestRequest request, T obj, params s return request; } - /// - /// Adds cookie to the cookie container. - /// - /// RestRequest to add the cookies to - /// Cookie name - /// Cookie value - /// Cookie path - /// Cookie domain, must not be an empty string - /// - public static RestRequest AddCookie(this RestRequest request, string name, string value, string path, string domain) { + public static RestRequest AddObjectStatic(this RestRequest request, T obj, params string[] includedProperties) where T : class => + request.AddParameters(TypeCache.GetParameters(obj, includedProperties)); + + public static RestRequest AddObjectStatic(this RestRequest request, T obj) where T : class => + request.AddParameters(TypeCache.GetParameters(obj)); + + /// + /// Adds cookie to the cookie container. + /// + /// RestRequest to add the cookies to + /// Cookie name + /// Cookie value + /// Cookie path + /// Cookie domain, must not be an empty string + /// + public static RestRequest AddCookie(this RestRequest request, string name, string value, string path, string domain) { request.CookieContainer ??= new CookieContainer(); request.CookieContainer.Add(new Cookie(name, value, path, domain)); return request; diff --git a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.ITestCase.cs b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.ITestCase.cs new file mode 100644 index 000000000..4f6788a36 --- /dev/null +++ b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.ITestCase.cs @@ -0,0 +1,24 @@ +using System.Collections; + +namespace RestSharp.Tests; + +public partial class ObjectParameterTests { + sealed partial class ParametersShouldBeStringified { + interface ITestCase { + public int Int32 { get; } + public string String { get; } + public DateTime DateTime { get; } + public object Object { get; } + public IEnumerable Ints { get; } + public IEnumerable Strings { get; } + public IEnumerable DateTimes { get; } + public IEnumerable Objects { get; } + public IEnumerable Enumerable { get; } + public IEnumerable> NestedStrings { get; } + public IEnumerable> NestedInts { get; } + public IEnumerable> NestedDateTimes { get; } + public IEnumerable> NestedObjects { get; } + public IEnumerable NestedEnumerables { get; } + } + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.NoAttributes.cs b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.NoAttributes.cs new file mode 100644 index 000000000..9819ce2ff --- /dev/null +++ b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.NoAttributes.cs @@ -0,0 +1,24 @@ +using System.Collections; + +namespace RestSharp.Tests; + +public partial class ObjectParameterTests { + sealed partial class ParametersShouldBeStringified { + sealed record NoAttributes( + int Int32, + string String, + DateTime DateTime, + object Object, + IEnumerable Ints, + IEnumerable Strings, + IEnumerable DateTimes, + IEnumerable Objects, + IEnumerable Enumerable, + IEnumerable> NestedStrings, + IEnumerable> NestedInts, + IEnumerable> NestedDateTimes, + IEnumerable> NestedObjects, + IEnumerable NestedEnumerables) : + ITestCase; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.WithAttributes.cs b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.WithAttributes.cs new file mode 100644 index 000000000..6fbd60866 --- /dev/null +++ b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.WithAttributes.cs @@ -0,0 +1,24 @@ +using System.Collections; + +namespace RestSharp.Tests; + +public partial class ObjectParameterTests { + sealed partial class ParametersShouldBeStringified { + sealed record WithAttributes( + [property: RequestProperty(Name = "Integer", Format = "00000")] int Int32, + [property: RequestProperty(Name = "Text")] string String, + [property: RequestProperty(Name = "Date", Format = "dddd, dd MMMM yyyy")] DateTime DateTime, + [property: RequestProperty(Name = "FloatingPointNumbersCsv", Format = "0.00")] object Object, + [property: RequestProperty(Name = "IntegersCsv", Format = "000")] IEnumerable Ints, + [property: RequestProperty(Name = "TextsCsv", ArrayQueryType = RequestArrayQueryType.CommaSeparated)] IEnumerable Strings, + [property: RequestProperty(Name = "TimesArray", Format = "hh:mm tt", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] IEnumerable DateTimes, + [property: RequestProperty(Name = "GuidsArray", Format = "N", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] IEnumerable Objects, + [property: RequestProperty(Name = "CurrencyAmountsCsv", Format = "c2", ArrayQueryType = RequestArrayQueryType.CommaSeparated)] IEnumerable Enumerable, + [property: RequestProperty(Name = "FlattenedTextsArray", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] IEnumerable> NestedStrings, + [property: RequestProperty(Name = "FlattenedIntsCsv", ArrayQueryType = RequestArrayQueryType.CommaSeparated)] IEnumerable> NestedInts, + [property: RequestProperty(Name = "FlattenedTimesArray", Format = "hh:mm", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] IEnumerable> NestedDateTimes, + [property: RequestProperty(Name = "FlattenedObjectsCsv")] IEnumerable> NestedObjects, + [property: RequestProperty(Name = "FlattenedObjectsArray", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] IEnumerable NestedEnumerables) : + ITestCase; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.cs b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.cs new file mode 100644 index 000000000..bdc72d897 --- /dev/null +++ b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.cs @@ -0,0 +1,112 @@ +using RestSharp.Tests.SampleData; +using System.Globalization; + +namespace RestSharp.Tests; + +public partial class ObjectParameterTests { + sealed partial class ParametersShouldBeStringified : TestData { + static readonly object[] TestNoAttributes = + new object[] { + (RestRequest req) => { + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + + return req.AddObjectStatic(new NoAttributes( + Int32: 1, + String: "Hello world", + DateTime: DateTime.Parse("11/11/2022 14:19:00"), + Object: Guid.Parse("c3e00f68-a4b3-4bbc-bcbd-5844d5490822"), + Ints: new[] { 2, 3, 4, 5 }, + Strings: new[] { "Hello", "world", "from", "C#" }, + DateTimes: new[] { DateTime.Parse("04/04/2002 13:45:55"), DateTime.Parse("10/10/2022 14:19:00"), DateTime.Parse("01/01/2000 00:00:01") }, + Objects: new object[] { "Test this", 120, 33.333, DateTime.Parse("02/02/1992 13:13:13"), Guid.Parse("6c880d41-1f23-49e3-ade2-a5c4c12bdd9c") }, + Enumerable: new object[] { "Test that", 12.333m, DateTime.Parse("10/10/2010 14:14:22"), Guid.Parse("1ff6942d-079f-4cb3-a617-67406183849f") }, + NestedStrings: new string[][] { new[] { "Hello", "world" }, new[] { "from", "C#" } }, + NestedInts: new int[][]{ new[] { 1, 2, 3 }, new[] { 4, 5, 6 } }, + NestedDateTimes: new DateTime[][]{ new[] { DateTime.Parse("09/08/1988 14:19:00"), DateTime.Parse("09/08/1922 22:19:33") }, new[] { DateTime.Parse("04/05/2006 03:03:03") } }, + NestedObjects: new object[][]{ new object[] { 1, 2, 3 }, new[] { "Hello", "world" }, new object[] { Guid.Parse("b030a2bd-9382-4c6d-8322-11e461bea03e") }, new object[] { 'A', 1, "Make" } }, + NestedEnumerables: new object[][]{ new object[] { 'a', 'b', "letter c", 4, DateTime.Parse("07/04/1978 09:30:58"), Guid.Parse("71690435-5b08-415f-a551-766143197e31") }, new object[] { new object[] { "Awesome", new[] { 1, 2, 3 } } } })); + }, + new GetOrPostParameter[] { + new("Int32", "1"), + new("String", "Hello world"), + new("DateTime", "11/11/2022 14:19:00"), + new("Object", "c3e00f68-a4b3-4bbc-bcbd-5844d5490822"), + new("Ints", "2,3,4,5"), + new("Strings", "Hello,world,from,C#"), + new("DateTimes", "04/04/2002 13:45:55,10/10/2022 14:19:00,01/01/2000 00:00:01"), + new("Objects", "Test this,120,33.333,02/02/1992 13:13:13,6c880d41-1f23-49e3-ade2-a5c4c12bdd9c"), + new("Enumerable", "Test that,12.333,10/10/2010 14:14:22,1ff6942d-079f-4cb3-a617-67406183849f"), + new("NestedStrings", "Hello,world,from,C#"), + new("NestedInts", "1,2,3,4,5,6"), + new("NestedDateTimes", "09/08/1988 14:19:00,09/08/1922 22:19:33,04/05/2006 03:03:03"), + new("NestedObjects", "1,2,3,Hello,world,b030a2bd-9382-4c6d-8322-11e461bea03e,A,1,Make"), + new("NestedEnumerables", "a,b,letter c,4,07/04/1978 09:30:58,71690435-5b08-415f-a551-766143197e31,Awesome,1,2,3") + } + }; + + static readonly object[] TestWithAttributes = + new object[] { + (RestRequest req) => { + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + + return req.AddObjectStatic(new WithAttributes( + Int32: 132, + String: "Goodbye world", + DateTime: DateTime.Parse("03/03/2003 16:45:33"), + Object: new object[] { 12f, 13.333d, 23.666m, new[] { 44f, 66f } }, + Ints: new[] { 3, 4, 5, 6, 100, 1001 }, + Strings: new[] { "Goodbye", "world", "from", ".NET" }, + DateTimes: new[] { DateTime.Parse("11/10/2033 14:25:00"), DateTime.Parse("11/11/2011 19:45:00"), DateTime.Parse("04/04/2006 09:00:00") }, + Objects: new object[]{ Guid.Parse("f90b8c60-a7f7-4cea-bea4-360a5dcd5ec9"), Guid.Parse("7b692a2a-2398-4b60-a374-dc64aff3feb0"), new[] { Guid.Parse("0dbf0389-5337-436d-8293-ca3a276abd72"), Guid.Parse("89f66976-8d82-4972-a062-f7a3f141446f") } }, + Enumerable: new object[]{ 100, 333, 1_000_000, 123.33f, 33_012_123.99d, 777.21m, new[] { 11.333399f, 23f, 30f } }, + NestedStrings: new string[][]{ new[] { "I", "am", "being", "tested" }, new[] { "from", "C#", ".NET" } }, + NestedInts: new int[][] { new[] { 10, 20, 30 }, new[] { 33, 66, 99 } }, + NestedDateTimes: new DateTime[][]{ new[] { DateTime.Parse("09/04/2008 03:33:03"), DateTime.Parse("10/03/03 12:09:33") }, new[] { DateTime.Parse("05/06/2013 14:45:00") } }, + NestedObjects: new object[][] { new[] { "Hello", "world" }, new object[] { 1, 2, 3 }, new object[] { Guid.Parse("de5bb701-3830-48c6-a44c-fcce3384f03e") }, new object[] { "Good", 'C', 300m } }, + NestedEnumerables: new object[][] { new object[] { "This", Guid.Parse("4439bad3-4e69-41f7-bc8b-a7a6c4e6d81a"), DateTime.Parse("08/07/2014 09:08:02") }, new object[] { 1, 2, 3, new[] { 'A', 'B' } } })); + }, + new GetOrPostParameter[] { + new("Integer", "00132"), + new("Text", "Goodbye world"), + new("Date", "Monday, 03 March 2003"), + new("FloatingPointNumbersCsv", "12.00,13.33,23.67,44.00,66.00"), + new("IntegersCsv", "003,004,005,006,100,1001"), + new("TextsCsv", "Goodbye,world,from,.NET"), + new("TimesArray[]", "02:25 PM"), + new("TimesArray[]", "07:45 PM"), + new("TimesArray[]", "09:00 AM"), + new("GuidsArray[]", "f90b8c60a7f74ceabea4360a5dcd5ec9"), + new("GuidsArray[]", "7b692a2a23984b60a374dc64aff3feb0"), + new("GuidsArray[]", "0dbf03895337436d8293ca3a276abd72"), + new("GuidsArray[]", "89f669768d824972a062f7a3f141446f"), + new("CurrencyAmountsCsv", "¤100.00,¤333.00,¤1,000,000.00,¤123.33,¤33,012,123.99,¤777.21,¤11.33,¤23.00,¤30.00"), + new("FlattenedTextsArray[]", "I"), + new("FlattenedTextsArray[]", "am"), + new("FlattenedTextsArray[]", "being"), + new("FlattenedTextsArray[]", "tested"), + new("FlattenedTextsArray[]", "from"), + new("FlattenedTextsArray[]", "C#"), + new("FlattenedTextsArray[]", ".NET"), + new("FlattenedIntsCsv", "10,20,30,33,66,99"), + new("FlattenedTimesArray[]", "03:33"), + new("FlattenedTimesArray[]", "12:09"), + new("FlattenedTimesArray[]", "02:45"), + new("FlattenedObjectsCsv", "Hello,world,1,2,3,de5bb701-3830-48c6-a44c-fcce3384f03e,Good,C,300"), + new("FlattenedObjectsArray[]", "This"), + new("FlattenedObjectsArray[]", "4439bad3-4e69-41f7-bc8b-a7a6c4e6d81a"), + new("FlattenedObjectsArray[]", "08/07/2014 09:08:02"), + new("FlattenedObjectsArray[]", "1"), + new("FlattenedObjectsArray[]", "2"), + new("FlattenedObjectsArray[]", "3"), + new("FlattenedObjectsArray[]", "A"), + new("FlattenedObjectsArray[]", "B") + } + }; + + private protected override IEnumerable GetData() => + new object[][] { + TestNoAttributes, + TestWithAttributes + }; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/ObjectParameterTests.cs b/test/RestSharp.Tests/ObjectParameterTests.cs index 2be042e6b..0f5fe4393 100644 --- a/test/RestSharp.Tests/ObjectParameterTests.cs +++ b/test/RestSharp.Tests/ObjectParameterTests.cs @@ -1,11 +1,20 @@ +using System.Collections; + namespace RestSharp.Tests; -public class ObjectParameterTests { +public partial class ObjectParameterTests { [Fact] public void Can_Add_Object_With_IntegerArray_property() { var request = new RestRequest(); - var items = new[] { 2, 3, 4 }; + var items = new[] { 2, 3, 4 }; request.AddObject(new { Items = items }); request.Parameters.First().Should().Be(new GetOrPostParameter("Items", string.Join(",", items))); } -} \ No newline at end of file + + [Theory] + [ClassData(typeof(ParametersShouldBeStringified))] + public void Can_Add_Object_Static(Func populate, IEnumerable expectedParameters) { + var request = populate(new RestRequest()); + request.Parameters.Should().BeEquivalentTo(expectedParameters); + } +} diff --git a/test/RestSharp.Tests/SampleData/TestData.cs b/test/RestSharp.Tests/SampleData/TestData.cs new file mode 100644 index 000000000..f24984beb --- /dev/null +++ b/test/RestSharp.Tests/SampleData/TestData.cs @@ -0,0 +1,9 @@ +using System.Collections; + +namespace RestSharp.Tests.SampleData; +internal abstract class TestData : IEnumerable { + private protected abstract IEnumerable GetData(); + public IEnumerator GetEnumerator() => GetData().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} From 9609edc8e1a758082eb4bb778e84c7b5071c37be Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Wed, 16 Nov 2022 16:02:38 -0300 Subject: [PATCH 02/12] Add benchmarks to compare RestRequestExtensions.AddObjectStatic to RestRequestExtensions.AddObject --- ...bjectToRequestParametersBenchmarks.Data.cs | 5 +++ .../AddObjectToRequestParametersBenchmarks.cs | 35 +++++++++++++++++++ ...RestRequestExtensions.ParameterProperty.cs | 2 -- 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.Data.cs create mode 100644 benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs diff --git a/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.Data.cs b/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.Data.cs new file mode 100644 index 000000000..6ab91aa43 --- /dev/null +++ b/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.Data.cs @@ -0,0 +1,5 @@ +namespace RestSharp.Benchmarks.Requests { + public partial class AddObjectToRequestParametersBenchmarks { + sealed record Data(string String, int Int32, string[] Strings, int[] Ints); + } +} diff --git a/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs b/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs new file mode 100644 index 000000000..edaeccd63 --- /dev/null +++ b/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs @@ -0,0 +1,35 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; + +namespace RestSharp.Benchmarks.Requests { + [MemoryDiagnoser, RankColumn, Orderer(SummaryOrderPolicy.FastestToSlowest)] + public partial class AddObjectToRequestParametersBenchmarks { + Data _data; + string[] _fields; + + [GlobalSetup] + public void GlobalSetup() { + const string @string = "random string"; + const int arraySize = 10_000; + var strings = new string[arraySize]; + Array.Fill(strings, @string); + var ints = new int[arraySize]; + Array.Fill(ints, int.MaxValue); + + _data = new Data(@string, int.MaxValue, strings, ints); + _fields = new[] { nameof(Data.String), nameof(Data.Int32), nameof(Data.Strings), nameof(Data.Ints) }; + } + + [Benchmark(Baseline = true)] + public void AddObject() => new RestRequest().AddObject(_data); + + [Benchmark] + public void AddObjectStatic() => new RestRequest().AddObjectStatic(_data); + + [Benchmark] + public void AddObject_Filtered() => new RestRequest().AddObject(_data, _fields); + + [Benchmark] + public void AddObjectStatic_Filtered() => new RestRequest().AddObjectStatic(_data, _fields); + } +} diff --git a/src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs b/src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs index 6899db28a..a70f7de9f 100644 --- a/src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs +++ b/src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Reflection; - namespace RestSharp; public static partial class RestRequestExtensions { From c0794e8ca69c02ef8a00f6f80ab880eb05c32932 Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Fri, 18 Nov 2022 10:12:20 -0300 Subject: [PATCH 03/12] Improve performance of RestRequestExtensions.AddObjectStatic by removing unnecessary middle steps --- ...RestRequestExtensions.ParameterProperty.cs | 19 - ...PropertyCache.Populator.RequestProperty.cs | 52 ++ ...questExtensions.PropertyCache.Populator.cs | 297 ++++++++ .../RestRequestExtensions.PropertyCache.cs | 49 ++ .../RestRequestExtensions.TypeCache.cs | 220 ------ .../Request/RestRequestExtensions.cs | 4 +- .../ObjectParameterTests.ArrayData.cs | 5 + .../ObjectParameterTests.CsvData.cs | 5 + .../ObjectParameterTests.FormattedData.cs | 5 + .../ObjectParameterTests.NamedData.cs | 5 + ...ParametersShouldBeStringified.ITestCase.cs | 24 - ...ametersShouldBeStringified.NoAttributes.cs | 24 - ...etersShouldBeStringified.WithAttributes.cs | 24 - ...eterTests.ParametersShouldBeStringified.cs | 112 --- test/RestSharp.Tests/ObjectParameterTests.cs | 693 +++++++++++++++++- test/RestSharp.Tests/SampleData/TestData.cs | 9 - 16 files changed, 1107 insertions(+), 440 deletions(-) delete mode 100644 src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs create mode 100644 src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.RequestProperty.cs create mode 100644 src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs create mode 100644 src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs delete mode 100644 src/RestSharp/Request/RestRequestExtensions.TypeCache.cs create mode 100644 test/RestSharp.Tests/ObjectParameterTests.ArrayData.cs create mode 100644 test/RestSharp.Tests/ObjectParameterTests.CsvData.cs create mode 100644 test/RestSharp.Tests/ObjectParameterTests.FormattedData.cs create mode 100644 test/RestSharp.Tests/ObjectParameterTests.NamedData.cs delete mode 100644 test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.ITestCase.cs delete mode 100644 test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.NoAttributes.cs delete mode 100644 test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.WithAttributes.cs delete mode 100644 test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.cs delete mode 100644 test/RestSharp.Tests/SampleData/TestData.cs diff --git a/src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs b/src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs deleted file mode 100644 index a70f7de9f..000000000 --- a/src/RestSharp/Request/RestRequestExtensions.ParameterProperty.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) .NET Foundation and Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace RestSharp; - -public static partial class RestRequestExtensions { - sealed record ParameterProperty(string Name, IEnumerable Values); -} diff --git a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.RequestProperty.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.RequestProperty.cs new file mode 100644 index 000000000..def6aeb2c --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.RequestProperty.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Reflection; + +namespace RestSharp; + +public static partial class RestRequestExtensions { + static partial class PropertyCache where T : class { + sealed partial class Populator { + sealed record RequestProperty { + internal string Name { get; init; } + internal string? Format { get; } + internal RequestArrayQueryType ArrayQueryType { get; } + internal Type Type { get; } + + private RequestProperty(string name, string? format, RequestArrayQueryType arrayQueryType, Type type) { + Name = name; + Format = format; + ArrayQueryType = arrayQueryType; + Type = type; + } + + internal static RequestProperty From(PropertyInfo property) { + var requestPropertyAttribute = + property.GetCustomAttribute() ?? + new RequestPropertyAttribute(); + + var propertyName = requestPropertyAttribute.Name ?? property.Name; + + return new RequestProperty( + propertyName, + requestPropertyAttribute.Format, + requestPropertyAttribute.ArrayQueryType, + property.PropertyType); + } + } + + } + } +} diff --git a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs new file mode 100644 index 000000000..a80a92c75 --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs @@ -0,0 +1,297 @@ +// Copyright (c) .NET Foundation and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace RestSharp; + +public static partial class RestRequestExtensions { + static partial class PropertyCache where T : class { + sealed partial class Populator { + internal string PropertyName { get; } + readonly Action> _populate; + + private Populator(string propertyName, Action> populate) { + PropertyName = propertyName; + _populate = populate; + } + + internal void Populate(T entity, ICollection parameters) => _populate(entity, parameters); + + internal static Populator From(PropertyInfo property) { + var entity = Expression.Parameter(typeof(T)); + var callGetter = Expression.Call(entity, property.GetGetMethod()!); + + var convertGetterReturnToObject = Expression.Convert(callGetter, typeof(object)); + + var getObject = Expression.Lambda>(convertGetterReturnToObject, entity).Compile(); + + var populate = GetPopulate(getObject, property); + + return new Populator(property.Name, populate); + } + + static Action> GetPopulate(Func getFormattable, RequestProperty requestProperty) => (model, parameters) => Populate(getFormattable(model), requestProperty, parameters); + + static Action> GetPopulate(Func getConvertible, RequestProperty requestProperty) => (model, parameters) => Populate(getConvertible(model), requestProperty, parameters); + + static Action> GetPopulate(Func> getFormattables, RequestProperty requestProperty) => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => (model, parameters) => PopulateCsv(getFormattables(model), requestProperty, parameters), + RequestArrayQueryType.ArrayParameters => GetPopulateArray(getFormattables, requestProperty), + _ => (_, _) => { } + }; + + static Action> GetPopulate(Func> getConvertibles, RequestProperty requestProperty) => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsv(getConvertibles(entity), requestProperty, parameters), + RequestArrayQueryType.ArrayParameters => GetPopulateArray(getConvertibles, requestProperty), + _ => (_, _) => { } + }; + + static Action> GetPopulate(Func> getObjects, RequestProperty requestProperty) => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsv(getObjects(entity), requestProperty, parameters), + RequestArrayQueryType.ArrayParameters => GetPopulateArray(getObjects, requestProperty), + _ => (_, _) => { } + }; + + static Action> GetPopulate(Func getObject, RequestProperty requestProperty) => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsv(getObject(entity), requestProperty, parameters), + RequestArrayQueryType.ArrayParameters => (entity, parameters) => PopulateArray(getObject(entity), requestProperty, parameters), + _ => (_, _) => { } + }; + + static Action> GetPopulate(Func getObject, PropertyInfo property) { + var requestProperty = RequestProperty.From(property); + + return property.PropertyType switch { + var formattableType when typeof(IFormattable).IsAssignableFrom(formattableType) => GetPopulate(entity => Unsafe.As(getObject(entity))!, requestProperty), + var convertibleType when typeof(IConvertible).IsAssignableFrom(convertibleType) => GetPopulate(entity => Unsafe.As(getObject(entity))!, requestProperty), + var enumerableType when typeof(IEnumerable).IsAssignableFrom(enumerableType) => GetPopulateUnknown(entity => Unsafe.As(getObject(entity))!, requestProperty), + var otherType => GetPopulate(getObject, requestProperty) + }; + } + + static Action> GetPopulateUnknown(Func getEnumerable, RequestProperty requestProperty) { + + if (GetSingleEnumeratedTypeOrNull(requestProperty.Type) is not { } enumeratedType) { + return GetPopulateKnown(getEnumerable, requestProperty); + } + + return enumeratedType switch { + var formattableEnumeratedType when typeof(IFormattable).IsAssignableFrom(formattableEnumeratedType) => GetPopulate(BoxInto(getEnumerable, formattableEnumeratedType), requestProperty), + var convertibleEnumeratedType when typeof(IConvertible).IsAssignableFrom(convertibleEnumeratedType) => GetPopulate(BoxInto(getEnumerable, convertibleEnumeratedType), requestProperty), + var otherEnumeratedType => GetPopulate(BoxInto(getEnumerable, otherEnumeratedType), requestProperty) + }; + } + + static Action> GetPopulateKnown(Func getEnumerable, RequestProperty requestProperty) => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsvUnknown(getEnumerable(entity), requestProperty, parameters), + RequestArrayQueryType.ArrayParameters => GetPopulateArray(getEnumerable, requestProperty), + _ => (_, _) => { } + }; + + static Action> GetPopulateArray(Func> getFormattables, RequestProperty requestProperty) => + GetPopulateArray(getFormattables, formattable => GetStringValue(formattable, requestProperty), requestProperty); + + static Action> GetPopulateArray(Func> getConvertibles, RequestProperty requestProperty) => + GetPopulateArray(getConvertibles, GetStringValue, requestProperty); + + static Action> GetPopulateArray(Func> getObjects, RequestProperty requestProperty) => + GetPopulateArray(getObjects, @object => GetUnknownStringValue(@object, requestProperty), requestProperty); + + static Action> GetPopulateArray(Func> getEnumerable, Func toString, RequestProperty requestProperty) where V : class { + var newRequestProperty = requestProperty with { Name = $"{requestProperty.Name}[]" }; + return (entity, parameters) => PopulateArray(getEnumerable(entity), toString, newRequestProperty, parameters); + } + + static Action> GetPopulateArray(Func getEnumerable, RequestProperty requestProperty) => + GetPopulateArray((entity) => getEnumerable(entity).Cast(), requestProperty); + + static void Populate(IFormattable formattable, RequestProperty requestProperty, ICollection parameters) => Populate(GetStringValue(formattable, requestProperty), requestProperty, parameters); + + static void Populate(IConvertible convertible, RequestProperty requestProperty, ICollection parameters) => Populate(GetStringValue(convertible), requestProperty, parameters); + + static void Populate(object @object, RequestProperty requestProperty, ICollection parameters) => Populate(GetKnownStringValue(@object), requestProperty, parameters); + + static void Populate(string? stringValue, RequestProperty requestProperty, ICollection parameters) { + var parameter = new GetOrPostParameter(requestProperty.Name, stringValue); + parameters.Add(parameter); + } + + static void PopulateCsv(IEnumerable formattables, RequestProperty requestProperty, ICollection parameters) => + PopulateCsv(formattables, formattable => GetStringValue(formattable, requestProperty), requestProperty, parameters); + + static void PopulateCsv(IEnumerable convertibles, RequestProperty requestProperty, ICollection parameters) => + PopulateCsv(convertibles, GetStringValue, requestProperty, parameters); + + static void PopulateCsv(IEnumerable objects, RequestProperty requestProperty, ICollection parameters) => + PopulateCsv(objects, @object => GetUnknownStringValue(@object, requestProperty), requestProperty, parameters); + + static void PopulateCsv(IEnumerable enumerable, Func toString, RequestProperty requestProperty, ICollection parameters) where V : class { + const string csvSeparator = ","; + var formattedStrings = enumerable.Select(toString); + var csv = string.Join(csvSeparator, formattedStrings); + Populate(csv, requestProperty, parameters); + } + + static void PopulateCsv(object @object, RequestProperty requestProperty, ICollection parameters) { + switch (@object) { + case IFormattable formattable: + Populate(formattable, requestProperty, parameters); + break; + case IConvertible convertible: + Populate(convertible, requestProperty, parameters); + break; + case IEnumerable formattables: + PopulateCsv(formattables, requestProperty, parameters); + break; + case IEnumerable convertibles: + PopulateCsv(convertibles, requestProperty, parameters); + break; + case IEnumerable objects: + PopulateCsv(objects, requestProperty, parameters); + break; + case IEnumerable enumerable: + PopulateCsvUnknown(enumerable, requestProperty, parameters); + break; + default: + Populate(@object, requestProperty, parameters); + break; + } + } + + static void PopulateCsvKnown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) => + PopulateCsv(enumerable.Cast(), requestProperty, parameters); + + static void PopulateCsvUnknown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { + + if (GetSingleEnumeratedTypeOrNull(enumerable.GetType()) is not { } enumeratedType) { + PopulateCsvKnown(enumerable, requestProperty, parameters); + return; + } + + switch (enumeratedType) { + case var _ when typeof(IFormattable).IsAssignableFrom(enumeratedType): + PopulateCsv(enumerable.Cast(), requestProperty, parameters); + break; + case var _ when typeof(IConvertible).IsAssignableFrom(enumeratedType): + PopulateCsv(enumerable.Cast(), requestProperty, parameters); + break; + default: + PopulateCsvKnown(enumerable, requestProperty, parameters); + break; + } + } + + static void PopulateArray(IEnumerable formattables, RequestProperty requestProperty, ICollection parameters) => + PopulateArray(formattables, formattable => GetStringValue(formattable, requestProperty), requestProperty, parameters); + + static void PopulateArray(IEnumerable convertibles, RequestProperty requestProperty, ICollection parameters) => + PopulateArray(convertibles, GetStringValue, requestProperty, parameters); + + static void PopulateArray(IEnumerable objects, RequestProperty requestProperty, ICollection parameters) => + PopulateArray(objects, @object => GetUnknownStringValue(@object, requestProperty), requestProperty, parameters); + + static void PopulateArray(IEnumerable enumerable, Func toString, RequestProperty requestProperty, ICollection parameters) where V : class { + var values = enumerable.Select(toString); + + foreach (var value in values) { + Populate(value, requestProperty, parameters); + } + } + + static void PopulateArray(object @object, RequestProperty requestProperty, ICollection parameters) { + switch (@object) { + case IFormattable formattable: + Populate(formattable, requestProperty, parameters); + break; + case IConvertible convertible: + Populate(convertible, requestProperty, parameters); + break; + case IEnumerable enumerable: + requestProperty = requestProperty with { Name = $"{requestProperty.Name}[]" }; + switch (enumerable) { + case IEnumerable formattables: + PopulateArray(formattables, requestProperty, parameters); + break; + case IEnumerable convertibles: + PopulateArray(convertibles, requestProperty, parameters); + break; + case IEnumerable objects: + PopulateArray(objects, requestProperty, parameters); + break; + default: + PopulateArrayUnknown(enumerable, requestProperty, parameters); + break; + } + break; + default: + Populate(@object, requestProperty, parameters); + break; + } + } + + static void PopulateArrayKnown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) => + PopulateArray(enumerable.Cast(), requestProperty, parameters); + + static void PopulateArrayUnknown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { + + if (GetSingleEnumeratedTypeOrNull(enumerable.GetType()) is not { } enumeratedType) { + PopulateArrayKnown(enumerable, requestProperty, parameters); + return; + } + + switch (enumeratedType) { + case var _ when typeof(IFormattable).IsAssignableFrom(enumeratedType): + PopulateArray(enumerable.Cast(), requestProperty, parameters); + break; + case var _ when typeof(IConvertible).IsAssignableFrom(enumeratedType): + PopulateArray(enumerable.Cast(), requestProperty, parameters); + break; + default: + PopulateArrayKnown(enumerable, requestProperty, parameters); + break; + } + } + + static string GetStringValue(IFormattable formattable, RequestProperty requestProperty) => formattable.ToString(requestProperty.Format, null); + static string GetStringValue(IConvertible convertible) => convertible.ToString(null); + static string? GetKnownStringValue(object @object) => TypeDescriptor.GetConverter(@object).ConvertToString(@object); + static string? GetUnknownStringValue(object @object, RequestProperty requestProperty) => @object switch { + IFormattable formattable => GetStringValue(formattable, requestProperty), + IConvertible convertible => GetStringValue(convertible), + _ => GetKnownStringValue(@object) + }; + + static Func> BoxInto(Func getEnumerable, Type enumeratedType) where V : class => + enumeratedType.IsValueType ? + entity => getEnumerable(entity).Cast() : + entity => Unsafe.As>(getEnumerable(entity))!; + + static Type? GetSingleEnumeratedTypeOrNull(Type enumerableType) { + var enumerableInterfaces = + enumerableType + .GetInterfaces() + .Where(@interface => @interface.IsGenericType) + .Where(@interface => @interface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + .ToArray(); + + return enumerableInterfaces.Length == 1 ? enumerableInterfaces[0].GetGenericArguments()[0] : null; + } + } + } +} diff --git a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs new file mode 100644 index 000000000..9f20f2569 --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Reflection; + +namespace RestSharp; + +public static partial class RestRequestExtensions { + static partial class PropertyCache where T : class { + static readonly IReadOnlyCollection Populators = + typeof(T) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(Populator.From) + .ToArray(); + + internal static IEnumerable GetParameters(T entity, params string[] includedProperties) { + if (includedProperties.Length == 0) { + return GetParameters(entity); + } + + Array.Sort(includedProperties); // Otherwise binary search is unsafe. + + var populators = Populators.Where(populator => Array.BinarySearch(includedProperties, populator.PropertyName) >= 0); + return GetParameters(entity, populators); + } + internal static IEnumerable GetParameters(T entity) => GetParameters(entity, Populators); + + static IEnumerable GetParameters(T entity, IEnumerable populators) { + var parameters = new List(capacity: Populators.Count); + + foreach (var populator in populators) { + populator.Populate(entity, parameters); + } + + return parameters; + } + } +} diff --git a/src/RestSharp/Request/RestRequestExtensions.TypeCache.cs b/src/RestSharp/Request/RestRequestExtensions.TypeCache.cs deleted file mode 100644 index 805037bcb..000000000 --- a/src/RestSharp/Request/RestRequestExtensions.TypeCache.cs +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright (c) .NET Foundation and Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using RestSharp.Extensions; -using System.Collections; -using System.ComponentModel; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.CompilerServices; - -namespace RestSharp; - -public static partial class RestRequestExtensions { - static partial class TypeCache where T : class { -#if NET5_0_OR_GREATER - const char CsvSeparator = ','; -#else - const string CsvSeparator = ","; -#endif - const string ArrayBrackets = "[]"; - - static readonly IReadOnlyDictionary> NameToGetter = - typeof(T) - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Select(property => { - var requestProperty = - property.GetAttribute() ?? - new RequestPropertyAttribute(); - - return new { - property.Name, - // The getter can only be null if it is non-public, - // but we are already filtering out non-public properties - // through the binding flags, so it's fine. - GetMethod = property.GetGetMethod()!, - RequestProperty = requestProperty - }; - }) -#if NET5_0_OR_GREATER - // We must filter out ref structs otherwise they will - // break LINQ expressions apart once we compile them. - .Where(property => !property.GetMethod.ReturnType.IsByRefLike) -#endif - .ToDictionary( - property => property.Name, - property => CompileGetter( - property.GetMethod, - property.RequestProperty.Name ?? property.Name, - property.RequestProperty)); - - internal static IEnumerable GetParameters(T obj, params string[] includedProperties) => - includedProperties.Length == 0 ? - GetParameters(obj) : - GetParameters(obj, includedProperties.Select(property => NameToGetter[property])); - - internal static IEnumerable GetParameters(T obj) => - GetParameters(obj, NameToGetter.Values); - - static IEnumerable GetParameters(T obj, IEnumerable> getters) => - getters - .Select(getParameter => getParameter(obj)) - .SelectMany(property => property.Values.Select(value => new GetOrPostParameter(property.Name, value))); - - static Func CompileGetter(MethodInfo getMethod, string propertyName, RequestPropertyAttribute requestProperty) { - var modelParameter = Expression.Parameter(typeof(T)); - - var callGetter = Expression.Call(modelParameter, getMethod); - - return getMethod.ReturnType switch { - var @return when typeof(IFormattable).IsAssignableFrom(@return) => - @return.IsValueType ? - GetFormattableGetter(modelParameter, Expression.Convert(callGetter, typeof(IFormattable)), propertyName, requestProperty) : - GetFormattableGetter(modelParameter, callGetter, propertyName, requestProperty), - var @return when typeof(IConvertible).IsAssignableFrom(@return) => - @return.IsValueType ? - GetConvertibleGetter(modelParameter, Expression.Convert(callGetter, typeof(IConvertible)), propertyName) : - GetConvertibleGetter(modelParameter, callGetter, propertyName), - var @return when typeof(IEnumerable).IsAssignableFrom(@return) => - @return.IsValueType ? - GetEnumerableGetter(modelParameter, Expression.Convert(callGetter, typeof(IEnumerable)), propertyName, requestProperty, @return) : - GetEnumerableGetter(modelParameter, callGetter, propertyName, requestProperty, @return), - var @return => - @return.IsValueType ? - GetObjectGetter(modelParameter, Expression.Convert(callGetter, typeof(object)), propertyName, requestProperty, @return) : - GetObjectGetter(modelParameter, callGetter, propertyName, requestProperty, @return) - }; - } - - static Func GetFormattableGetter(ParameterExpression modelParameter, Expression callGetter, string propertyName, RequestPropertyAttribute requestProperty) { - var getFormattable = Expression.Lambda>(callGetter, modelParameter).Compile(); - - return model => new ParameterProperty(propertyName, new[] { - getFormattable(model).ToString(requestProperty.Format, null) - }); - } - - static Func GetConvertibleGetter(ParameterExpression modelParameter, Expression callGetter, string propertyName) { - var getFormattable = Expression.Lambda>(callGetter, modelParameter).Compile(); - - return model => new ParameterProperty(propertyName, new[] { - getFormattable(model).ToString(null) - }); - } - - static Func GetEnumerableGetter(ParameterExpression modelParameter, Expression callGetter, string propertyName, RequestPropertyAttribute requestProperty, Type @return) { - - var getEnumerable = Expression.Lambda>(callGetter, modelParameter).Compile(); - - var getValues = GetGetFormattedStringValues(getEnumerable, requestProperty, @return); - - var actualPropertyName = - requestProperty.ArrayQueryType == RequestArrayQueryType.ArrayParameters ? - $"{propertyName}{ArrayBrackets}" : - propertyName; - - return model => new ParameterProperty(actualPropertyName, getValues(model)); - } - - static Func> GetGetFormattedStringValues(Func getEnumerable, RequestPropertyAttribute requestProperty, Type @return) { - - var getStringValues = GetGetRawStringValues(getEnumerable, requestProperty, @return); - - return requestProperty.ArrayQueryType switch { - RequestArrayQueryType.CommaSeparated => model => new[] { string.Join(CsvSeparator, getStringValues(model)) }, - RequestArrayQueryType.ArrayParameters => getStringValues, - _ => model => Enumerable.Empty() - }; - } - - static Func> GetGetRawStringValues(Func getEnumerable, RequestPropertyAttribute requestProperty, Type @return) { - var enumerableInterfaces = - @return - .GetInterfaces() - .Append(@return) // In case `@return` is `IEnumerable<>`: https://stackoverflow.com/a/63794667 - .Where( - @interface => - @interface.IsGenericType && - @interface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - .ToArray(); - - if (enumerableInterfaces.Length != 1) { - return requestProperty.ArrayQueryType switch { - RequestArrayQueryType.CommaSeparated => model => getEnumerable(model).Cast().Select(value => string.Join(CsvSeparator, GetStringValues(value, requestProperty))), - RequestArrayQueryType.ArrayParameters => model => getEnumerable(model).Cast().SelectMany(value => GetStringValues(value, requestProperty)), - _ => model => Enumerable.Empty() - }; - } - - return enumerableInterfaces[0].GetGenericArguments()[0] switch { - var enumerated when typeof(IFormattable).IsAssignableFrom(enumerated) => - enumerated.IsValueType ? - model => getEnumerable(model).Cast().Select(formattable => formattable.ToString(requestProperty.Format, null)) : - model => Unsafe.As>(getEnumerable(model)).Select(formattable => formattable.ToString(requestProperty.Format, null)), - var enumerated when typeof(IConvertible).IsAssignableFrom(enumerated) => - enumerated.IsValueType ? - model => getEnumerable(model).Cast().Select(formattable => formattable.ToString(null)) : - model => Unsafe.As>(getEnumerable(model)).Select(formattable => formattable.ToString(null)), - - var enumerated => - requestProperty.ArrayQueryType switch { - RequestArrayQueryType.CommaSeparated => - enumerated.IsValueType ? - model => new[] { string.Join(CsvSeparator, getEnumerable(model).Cast().Select(value => string.Join(CsvSeparator, GetStringValues(value, requestProperty)))) } : - model => new[] { string.Join(CsvSeparator, Unsafe.As>(getEnumerable(model)).Select(value => string.Join(CsvSeparator, GetStringValues(value, requestProperty)))) }, - RequestArrayQueryType.ArrayParameters => - enumerated.IsValueType ? - model => getEnumerable(model).Cast().SelectMany(value => GetStringValues(value, requestProperty)) : - model => Unsafe.As>(getEnumerable(model)).SelectMany(value => GetStringValues(value, requestProperty)), - _ => model => Enumerable.Empty() - } - }; - } - - static Func GetObjectGetter(ParameterExpression modelParameter, Expression callGetter, string propertyName, RequestPropertyAttribute requestProperty, Type @return) { - var getObject = Expression.Lambda>(callGetter, modelParameter).Compile(); - - Func getPropertyName = - requestProperty.ArrayQueryType == RequestArrayQueryType.ArrayParameters ? - @object => @object is IEnumerable ? $"{propertyName}{ArrayBrackets}" : propertyName : - _ => propertyName; - - if (@return == typeof(object)) { - return model => { - var @object = getObject(model); - - var values = GetStringValues(@object, requestProperty); - - return new ParameterProperty(getPropertyName(@object), values); - }; - } - - var getterConverter = TypeDescriptor.GetConverter(@return); - return model => new ParameterProperty(propertyName, new[] { - getterConverter.ConvertToString(getObject(model)) - }); - } - - static IEnumerable GetStringValues(object @object, RequestPropertyAttribute requestProperty) => @object switch { - IFormattable formattable => new[] { formattable.ToString(requestProperty.Format, null) }, - IConvertible convertible => new[] { convertible.ToString(null) }, - IEnumerable enumerable => requestProperty.ArrayQueryType switch { - RequestArrayQueryType.CommaSeparated => new[] { string.Join(CsvSeparator, enumerable.Cast().Select(value => string.Join(CsvSeparator, GetStringValues(value, requestProperty)))) }, - RequestArrayQueryType.ArrayParameters => enumerable.Cast().SelectMany(value => GetStringValues(value, requestProperty)), - _ => Enumerable.Empty() - }, - _ => new[] { TypeDescriptor.GetConverter(@object).ConvertToString(@object) } - }; - } -} diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index 80bee3c2c..e5e213147 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -452,10 +452,10 @@ public static RestRequest AddObject(this RestRequest request, T obj, params s } public static RestRequest AddObjectStatic(this RestRequest request, T obj, params string[] includedProperties) where T : class => - request.AddParameters(TypeCache.GetParameters(obj, includedProperties)); + request.AddParameters(PropertyCache.GetParameters(obj, includedProperties)); public static RestRequest AddObjectStatic(this RestRequest request, T obj) where T : class => - request.AddParameters(TypeCache.GetParameters(obj)); + request.AddParameters(PropertyCache.GetParameters(obj)); /// /// Adds cookie to the cookie container. diff --git a/test/RestSharp.Tests/ObjectParameterTests.ArrayData.cs b/test/RestSharp.Tests/ObjectParameterTests.ArrayData.cs new file mode 100644 index 000000000..be6b10782 --- /dev/null +++ b/test/RestSharp.Tests/ObjectParameterTests.ArrayData.cs @@ -0,0 +1,5 @@ +namespace RestSharp.Tests; + +public partial class ObjectParameterTests { + sealed record ArrayData([property: RequestProperty(ArrayQueryType = RequestArrayQueryType.ArrayParameters)] TEnumerable Array) where TEnumerable : notnull; +} diff --git a/test/RestSharp.Tests/ObjectParameterTests.CsvData.cs b/test/RestSharp.Tests/ObjectParameterTests.CsvData.cs new file mode 100644 index 000000000..7ae47f78a --- /dev/null +++ b/test/RestSharp.Tests/ObjectParameterTests.CsvData.cs @@ -0,0 +1,5 @@ +namespace RestSharp.Tests; + +public partial class ObjectParameterTests { + sealed record CsvData([property: RequestProperty(ArrayQueryType = RequestArrayQueryType.CommaSeparated)] TEnumerable Csv) where TEnumerable : notnull; +} diff --git a/test/RestSharp.Tests/ObjectParameterTests.FormattedData.cs b/test/RestSharp.Tests/ObjectParameterTests.FormattedData.cs new file mode 100644 index 000000000..c11c1d6b3 --- /dev/null +++ b/test/RestSharp.Tests/ObjectParameterTests.FormattedData.cs @@ -0,0 +1,5 @@ +namespace RestSharp.Tests; + +public partial class ObjectParameterTests { + sealed record FormattedData([property: RequestProperty(Format = "hh:mm tt")] TDateTime FormattedParameter) where TDateTime : notnull; +} diff --git a/test/RestSharp.Tests/ObjectParameterTests.NamedData.cs b/test/RestSharp.Tests/ObjectParameterTests.NamedData.cs new file mode 100644 index 000000000..908127dee --- /dev/null +++ b/test/RestSharp.Tests/ObjectParameterTests.NamedData.cs @@ -0,0 +1,5 @@ +namespace RestSharp.Tests; + +public partial class ObjectParameterTests { + sealed record NamedData([property: RequestProperty(Name = "CustomName")] object NamedParameter); +} diff --git a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.ITestCase.cs b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.ITestCase.cs deleted file mode 100644 index 4f6788a36..000000000 --- a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.ITestCase.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections; - -namespace RestSharp.Tests; - -public partial class ObjectParameterTests { - sealed partial class ParametersShouldBeStringified { - interface ITestCase { - public int Int32 { get; } - public string String { get; } - public DateTime DateTime { get; } - public object Object { get; } - public IEnumerable Ints { get; } - public IEnumerable Strings { get; } - public IEnumerable DateTimes { get; } - public IEnumerable Objects { get; } - public IEnumerable Enumerable { get; } - public IEnumerable> NestedStrings { get; } - public IEnumerable> NestedInts { get; } - public IEnumerable> NestedDateTimes { get; } - public IEnumerable> NestedObjects { get; } - public IEnumerable NestedEnumerables { get; } - } - } -} \ No newline at end of file diff --git a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.NoAttributes.cs b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.NoAttributes.cs deleted file mode 100644 index 9819ce2ff..000000000 --- a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.NoAttributes.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections; - -namespace RestSharp.Tests; - -public partial class ObjectParameterTests { - sealed partial class ParametersShouldBeStringified { - sealed record NoAttributes( - int Int32, - string String, - DateTime DateTime, - object Object, - IEnumerable Ints, - IEnumerable Strings, - IEnumerable DateTimes, - IEnumerable Objects, - IEnumerable Enumerable, - IEnumerable> NestedStrings, - IEnumerable> NestedInts, - IEnumerable> NestedDateTimes, - IEnumerable> NestedObjects, - IEnumerable NestedEnumerables) : - ITestCase; - } -} \ No newline at end of file diff --git a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.WithAttributes.cs b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.WithAttributes.cs deleted file mode 100644 index 6fbd60866..000000000 --- a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.WithAttributes.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections; - -namespace RestSharp.Tests; - -public partial class ObjectParameterTests { - sealed partial class ParametersShouldBeStringified { - sealed record WithAttributes( - [property: RequestProperty(Name = "Integer", Format = "00000")] int Int32, - [property: RequestProperty(Name = "Text")] string String, - [property: RequestProperty(Name = "Date", Format = "dddd, dd MMMM yyyy")] DateTime DateTime, - [property: RequestProperty(Name = "FloatingPointNumbersCsv", Format = "0.00")] object Object, - [property: RequestProperty(Name = "IntegersCsv", Format = "000")] IEnumerable Ints, - [property: RequestProperty(Name = "TextsCsv", ArrayQueryType = RequestArrayQueryType.CommaSeparated)] IEnumerable Strings, - [property: RequestProperty(Name = "TimesArray", Format = "hh:mm tt", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] IEnumerable DateTimes, - [property: RequestProperty(Name = "GuidsArray", Format = "N", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] IEnumerable Objects, - [property: RequestProperty(Name = "CurrencyAmountsCsv", Format = "c2", ArrayQueryType = RequestArrayQueryType.CommaSeparated)] IEnumerable Enumerable, - [property: RequestProperty(Name = "FlattenedTextsArray", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] IEnumerable> NestedStrings, - [property: RequestProperty(Name = "FlattenedIntsCsv", ArrayQueryType = RequestArrayQueryType.CommaSeparated)] IEnumerable> NestedInts, - [property: RequestProperty(Name = "FlattenedTimesArray", Format = "hh:mm", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] IEnumerable> NestedDateTimes, - [property: RequestProperty(Name = "FlattenedObjectsCsv")] IEnumerable> NestedObjects, - [property: RequestProperty(Name = "FlattenedObjectsArray", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] IEnumerable NestedEnumerables) : - ITestCase; - } -} \ No newline at end of file diff --git a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.cs b/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.cs deleted file mode 100644 index bdc72d897..000000000 --- a/test/RestSharp.Tests/ObjectParameterTests.ParametersShouldBeStringified.cs +++ /dev/null @@ -1,112 +0,0 @@ -using RestSharp.Tests.SampleData; -using System.Globalization; - -namespace RestSharp.Tests; - -public partial class ObjectParameterTests { - sealed partial class ParametersShouldBeStringified : TestData { - static readonly object[] TestNoAttributes = - new object[] { - (RestRequest req) => { - Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; - - return req.AddObjectStatic(new NoAttributes( - Int32: 1, - String: "Hello world", - DateTime: DateTime.Parse("11/11/2022 14:19:00"), - Object: Guid.Parse("c3e00f68-a4b3-4bbc-bcbd-5844d5490822"), - Ints: new[] { 2, 3, 4, 5 }, - Strings: new[] { "Hello", "world", "from", "C#" }, - DateTimes: new[] { DateTime.Parse("04/04/2002 13:45:55"), DateTime.Parse("10/10/2022 14:19:00"), DateTime.Parse("01/01/2000 00:00:01") }, - Objects: new object[] { "Test this", 120, 33.333, DateTime.Parse("02/02/1992 13:13:13"), Guid.Parse("6c880d41-1f23-49e3-ade2-a5c4c12bdd9c") }, - Enumerable: new object[] { "Test that", 12.333m, DateTime.Parse("10/10/2010 14:14:22"), Guid.Parse("1ff6942d-079f-4cb3-a617-67406183849f") }, - NestedStrings: new string[][] { new[] { "Hello", "world" }, new[] { "from", "C#" } }, - NestedInts: new int[][]{ new[] { 1, 2, 3 }, new[] { 4, 5, 6 } }, - NestedDateTimes: new DateTime[][]{ new[] { DateTime.Parse("09/08/1988 14:19:00"), DateTime.Parse("09/08/1922 22:19:33") }, new[] { DateTime.Parse("04/05/2006 03:03:03") } }, - NestedObjects: new object[][]{ new object[] { 1, 2, 3 }, new[] { "Hello", "world" }, new object[] { Guid.Parse("b030a2bd-9382-4c6d-8322-11e461bea03e") }, new object[] { 'A', 1, "Make" } }, - NestedEnumerables: new object[][]{ new object[] { 'a', 'b', "letter c", 4, DateTime.Parse("07/04/1978 09:30:58"), Guid.Parse("71690435-5b08-415f-a551-766143197e31") }, new object[] { new object[] { "Awesome", new[] { 1, 2, 3 } } } })); - }, - new GetOrPostParameter[] { - new("Int32", "1"), - new("String", "Hello world"), - new("DateTime", "11/11/2022 14:19:00"), - new("Object", "c3e00f68-a4b3-4bbc-bcbd-5844d5490822"), - new("Ints", "2,3,4,5"), - new("Strings", "Hello,world,from,C#"), - new("DateTimes", "04/04/2002 13:45:55,10/10/2022 14:19:00,01/01/2000 00:00:01"), - new("Objects", "Test this,120,33.333,02/02/1992 13:13:13,6c880d41-1f23-49e3-ade2-a5c4c12bdd9c"), - new("Enumerable", "Test that,12.333,10/10/2010 14:14:22,1ff6942d-079f-4cb3-a617-67406183849f"), - new("NestedStrings", "Hello,world,from,C#"), - new("NestedInts", "1,2,3,4,5,6"), - new("NestedDateTimes", "09/08/1988 14:19:00,09/08/1922 22:19:33,04/05/2006 03:03:03"), - new("NestedObjects", "1,2,3,Hello,world,b030a2bd-9382-4c6d-8322-11e461bea03e,A,1,Make"), - new("NestedEnumerables", "a,b,letter c,4,07/04/1978 09:30:58,71690435-5b08-415f-a551-766143197e31,Awesome,1,2,3") - } - }; - - static readonly object[] TestWithAttributes = - new object[] { - (RestRequest req) => { - Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; - - return req.AddObjectStatic(new WithAttributes( - Int32: 132, - String: "Goodbye world", - DateTime: DateTime.Parse("03/03/2003 16:45:33"), - Object: new object[] { 12f, 13.333d, 23.666m, new[] { 44f, 66f } }, - Ints: new[] { 3, 4, 5, 6, 100, 1001 }, - Strings: new[] { "Goodbye", "world", "from", ".NET" }, - DateTimes: new[] { DateTime.Parse("11/10/2033 14:25:00"), DateTime.Parse("11/11/2011 19:45:00"), DateTime.Parse("04/04/2006 09:00:00") }, - Objects: new object[]{ Guid.Parse("f90b8c60-a7f7-4cea-bea4-360a5dcd5ec9"), Guid.Parse("7b692a2a-2398-4b60-a374-dc64aff3feb0"), new[] { Guid.Parse("0dbf0389-5337-436d-8293-ca3a276abd72"), Guid.Parse("89f66976-8d82-4972-a062-f7a3f141446f") } }, - Enumerable: new object[]{ 100, 333, 1_000_000, 123.33f, 33_012_123.99d, 777.21m, new[] { 11.333399f, 23f, 30f } }, - NestedStrings: new string[][]{ new[] { "I", "am", "being", "tested" }, new[] { "from", "C#", ".NET" } }, - NestedInts: new int[][] { new[] { 10, 20, 30 }, new[] { 33, 66, 99 } }, - NestedDateTimes: new DateTime[][]{ new[] { DateTime.Parse("09/04/2008 03:33:03"), DateTime.Parse("10/03/03 12:09:33") }, new[] { DateTime.Parse("05/06/2013 14:45:00") } }, - NestedObjects: new object[][] { new[] { "Hello", "world" }, new object[] { 1, 2, 3 }, new object[] { Guid.Parse("de5bb701-3830-48c6-a44c-fcce3384f03e") }, new object[] { "Good", 'C', 300m } }, - NestedEnumerables: new object[][] { new object[] { "This", Guid.Parse("4439bad3-4e69-41f7-bc8b-a7a6c4e6d81a"), DateTime.Parse("08/07/2014 09:08:02") }, new object[] { 1, 2, 3, new[] { 'A', 'B' } } })); - }, - new GetOrPostParameter[] { - new("Integer", "00132"), - new("Text", "Goodbye world"), - new("Date", "Monday, 03 March 2003"), - new("FloatingPointNumbersCsv", "12.00,13.33,23.67,44.00,66.00"), - new("IntegersCsv", "003,004,005,006,100,1001"), - new("TextsCsv", "Goodbye,world,from,.NET"), - new("TimesArray[]", "02:25 PM"), - new("TimesArray[]", "07:45 PM"), - new("TimesArray[]", "09:00 AM"), - new("GuidsArray[]", "f90b8c60a7f74ceabea4360a5dcd5ec9"), - new("GuidsArray[]", "7b692a2a23984b60a374dc64aff3feb0"), - new("GuidsArray[]", "0dbf03895337436d8293ca3a276abd72"), - new("GuidsArray[]", "89f669768d824972a062f7a3f141446f"), - new("CurrencyAmountsCsv", "¤100.00,¤333.00,¤1,000,000.00,¤123.33,¤33,012,123.99,¤777.21,¤11.33,¤23.00,¤30.00"), - new("FlattenedTextsArray[]", "I"), - new("FlattenedTextsArray[]", "am"), - new("FlattenedTextsArray[]", "being"), - new("FlattenedTextsArray[]", "tested"), - new("FlattenedTextsArray[]", "from"), - new("FlattenedTextsArray[]", "C#"), - new("FlattenedTextsArray[]", ".NET"), - new("FlattenedIntsCsv", "10,20,30,33,66,99"), - new("FlattenedTimesArray[]", "03:33"), - new("FlattenedTimesArray[]", "12:09"), - new("FlattenedTimesArray[]", "02:45"), - new("FlattenedObjectsCsv", "Hello,world,1,2,3,de5bb701-3830-48c6-a44c-fcce3384f03e,Good,C,300"), - new("FlattenedObjectsArray[]", "This"), - new("FlattenedObjectsArray[]", "4439bad3-4e69-41f7-bc8b-a7a6c4e6d81a"), - new("FlattenedObjectsArray[]", "08/07/2014 09:08:02"), - new("FlattenedObjectsArray[]", "1"), - new("FlattenedObjectsArray[]", "2"), - new("FlattenedObjectsArray[]", "3"), - new("FlattenedObjectsArray[]", "A"), - new("FlattenedObjectsArray[]", "B") - } - }; - - private protected override IEnumerable GetData() => - new object[][] { - TestNoAttributes, - TestWithAttributes - }; - } -} \ No newline at end of file diff --git a/test/RestSharp.Tests/ObjectParameterTests.cs b/test/RestSharp.Tests/ObjectParameterTests.cs index 0f5fe4393..6a69f7ba3 100644 --- a/test/RestSharp.Tests/ObjectParameterTests.cs +++ b/test/RestSharp.Tests/ObjectParameterTests.cs @@ -1,20 +1,701 @@ using System.Collections; +using System.Globalization; namespace RestSharp.Tests; public partial class ObjectParameterTests { + public ObjectParameterTests() { + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + } + [Fact] - public void Can_Add_Object_With_IntegerArray_property() { + public void Can_Add_Object_with_IntegerArray_property() { var request = new RestRequest(); var items = new[] { 2, 3, 4 }; request.AddObject(new { Items = items }); request.Parameters.First().Should().Be(new GetOrPostParameter("Items", string.Join(",", items))); } - [Theory] - [ClassData(typeof(ParametersShouldBeStringified))] - public void Can_Add_Object_Static(Func populate, IEnumerable expectedParameters) { - var request = populate(new RestRequest()); - request.Parameters.Should().BeEquivalentTo(expectedParameters); + [Fact] + public void Can_Add_Object_Static_with_Integer_property() { + const int item = 1230; + var @object = new { Item = item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), "1230")); + } + + [Fact] + public void Can_Add_Object_Static_with_Integer_as_Object_property() { + const int item = 1230; + var @object = new { Item = (object)item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), "1230")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_property() { + var items = new[] { 1, 2, 3 }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_as_ObjectArray_property() { + var items = new object[] { 1, 2, 3 }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_as_Object_property() { + var items = new int[] { 1, 2, 3 }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_as_ObjectArray_as_Object_property() { + var items = new object[] { 1, 2, 3 }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_as_Enumerable_property() { + var items = new[] { 1, 2, 3 }; + var @object = new { Items = (IEnumerable)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_as_Enumerable_as_Object_property() { + IEnumerable items = new[] { 1, 2, 3 }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_String_property() { + const string item = "Hello world"; + var @object = new { Item = item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), item)); + } + + [Fact] + public void Can_Add_Object_Static_with_String_as_Object_property() { + const string item = "Hello world"; + var @object = new { Item = (object)item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), item)); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_property() { + var items = new[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_property() { + var items = new object[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_Object_property() { + var items = new[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_as_Object_property() { + var items = new object[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_Enumerable_property() { + var items = new[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = (IEnumerable)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_Enumerable_as_Object_property() { + IEnumerable items = new[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTime_property() { + var item = DateTime.Parse("09/08/2025 13:35:23"); + var @object = new { Item = item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), "09/08/2025 13:35:23")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTime_as_Object_property() { + var item = DateTime.Parse("04/06/2006 19:56:44"); + var @object = new { Item = (object)item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), "04/06/2006 19:56:44")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_property() { + var items = new[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_ObjectArray_property() { + var items = new object[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_Object_property() { + var items = new[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_ObjectArray_as_Object_property() { + var items = new object[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_Enumerable_property() { + var items = new[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = (IEnumerable)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_Enumerable_as_Object_property() { + IEnumerable items = new[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_ObjectArray_property() { + var items = new object[] { "Hello world", 120, DateTime.Parse("06/06/2006 17:49:21"), Guid.Parse("1970a57f-d7f8-45d7-a269-f20e329d9432") }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello world,120,06/06/2006 17:49:21,1970a57f-d7f8-45d7-a269-f20e329d9432")); + } + + [Fact] + public void Can_Add_Object_Static_with_ObjectArray_as_Object_property() { + var items = new object[] { "Hello world", 120, DateTime.Parse("06/06/2006 17:49:21"), Guid.Parse("1970a57f-d7f8-45d7-a269-f20e329d9432") }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello world,120,06/06/2006 17:49:21,1970a57f-d7f8-45d7-a269-f20e329d9432")); + } + + [Fact] + public void Can_Add_Object_Static_with_ObjectArray_as_Enumerable_property() { + var items = new object[] { "Hello world", 120, DateTime.Parse("06/06/2006 17:49:21"), Guid.Parse("1970a57f-d7f8-45d7-a269-f20e329d9432") }; + var @object = new { Items = (IEnumerable)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello world,120,06/06/2006 17:49:21,1970a57f-d7f8-45d7-a269-f20e329d9432")); + } + + [Fact] + public void Can_Add_Object_Static_with_ObjectArray_as_Enumerable_as_Object_property() { + IEnumerable items = new object[] { "Hello world", 120, DateTime.Parse("06/06/2006 17:49:21"), Guid.Parse("1970a57f-d7f8-45d7-a269-f20e329d9432") }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello world,120,06/06/2006 17:49:21,1970a57f-d7f8-45d7-a269-f20e329d9432")); + } + + [Fact] + public void Can_Add_Object_Static_with_custom_property_name() { + var item = new object[] { "Hello world", Array.Empty() }; + var namedData = new NamedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter("CustomName", "Hello world,Guid[] Array")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTime_using_custom_property_format() { + var item = DateTime.Parse("05/02/2020 09:12:33"); + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "09:12 AM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTime_as_Object_using_custom_property_format() { + var item = DateTime.Parse("05/02/2020 09:12:33"); + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "09:12 AM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_using_custom_property_format() { + var item = new[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_ObjectArray_using_custom_property_format() { + var item = new object[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_Object_using_custom_property_format() { + var item = new[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_ObjectArray_as_Object_using_custom_property_format() { + var item = new object[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_Enumerable_using_custom_property_format() { + var item = new[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_ObjectArray_as_Enumerable_using_custom_property_format() { + var item = new object[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_using_Csv_format() { + var items = new[] { "Hello", "world", "from", ".NET" }; + var namedData = new CsvData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(CsvData.Csv), "Hello,world,from,.NET")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_using_Csv_format() { + var items = new object[] { "Hello", "world", "from", ".NET" }; + var namedData = new CsvData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(CsvData.Csv), "Hello,world,from,.NET")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_Object_using_Csv_format() { + var items = new[] { "Hello", "world", "from", ".NET" }; + var namedData = new CsvData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(CsvData.Csv), "Hello,world,from,.NET")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_as_Object_using_Csv_format() { + var items = new object[] { "Hello", "world", "from", ".NET" }; + var namedData = new CsvData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(CsvData.Csv), "Hello,world,from,.NET")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_using_Array_format() { + var items = new[] { "Hello", "world", "from", ".NET" }; + var namedData = new ArrayData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .BeEquivalentTo(new[] { + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "Hello"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "world"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "from"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", ".NET"), + }); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_using_Array_format() { + var items = new object[] { "Hello", "world", "from", ".NET" }; + var namedData = new ArrayData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .BeEquivalentTo(new[] { + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "Hello"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "world"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "from"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", ".NET"), + }); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_Object_using_Array_format() { + var items = new[] { "Hello", "world", "from", ".NET" }; + var namedData = new ArrayData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .BeEquivalentTo(new[] { + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "Hello"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "world"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "from"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", ".NET"), + }); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_as_Object_using_Array_format() { + var items = new object[] { "Hello", "world", "from", ".NET" }; + var namedData = new ArrayData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .BeEquivalentTo(new[] { + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "Hello"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "world"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "from"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", ".NET"), + }); } } diff --git a/test/RestSharp.Tests/SampleData/TestData.cs b/test/RestSharp.Tests/SampleData/TestData.cs deleted file mode 100644 index f24984beb..000000000 --- a/test/RestSharp.Tests/SampleData/TestData.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections; - -namespace RestSharp.Tests.SampleData; -internal abstract class TestData : IEnumerable { - private protected abstract IEnumerable GetData(); - public IEnumerator GetEnumerator() => GetData().GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} From 17bbdd71561180d36e01b52aa0d21fe998ce691b Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Fri, 18 Nov 2022 11:08:31 -0300 Subject: [PATCH 04/12] Improve efficiency for conversion of IEnumerable to query parameters --- ...questExtensions.PropertyCache.Populator.cs | 100 +++++++++++------- 1 file changed, 60 insertions(+), 40 deletions(-) diff --git a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs index a80a92c75..c1719e5a9 100644 --- a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs @@ -37,7 +37,10 @@ internal static Populator From(PropertyInfo property) { var entity = Expression.Parameter(typeof(T)); var callGetter = Expression.Call(entity, property.GetGetMethod()!); - var convertGetterReturnToObject = Expression.Convert(callGetter, typeof(object)); + Expression convertGetterReturnToObject = + property.PropertyType.IsValueType ? + Expression.Convert(callGetter, typeof(object)) : + callGetter; var getObject = Expression.Lambda>(convertGetterReturnToObject, entity).Compile(); @@ -62,9 +65,9 @@ internal static Populator From(PropertyInfo property) { _ => (_, _) => { } }; - static Action> GetPopulate(Func> getObjects, RequestProperty requestProperty) => requestProperty.ArrayQueryType switch { - RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsv(getObjects(entity), requestProperty, parameters), - RequestArrayQueryType.ArrayParameters => GetPopulateArray(getObjects, requestProperty), + static Action> GetPopulate(Func getEnumerable, RequestProperty requestProperty) => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsv(getEnumerable(entity), requestProperty, parameters), + RequestArrayQueryType.ArrayParameters => GetPopulateArray(getEnumerable, requestProperty), _ => (_, _) => { } }; @@ -92,9 +95,9 @@ static Action> GetPopulateUnknown(Func } return enumeratedType switch { - var formattableEnumeratedType when typeof(IFormattable).IsAssignableFrom(formattableEnumeratedType) => GetPopulate(BoxInto(getEnumerable, formattableEnumeratedType), requestProperty), - var convertibleEnumeratedType when typeof(IConvertible).IsAssignableFrom(convertibleEnumeratedType) => GetPopulate(BoxInto(getEnumerable, convertibleEnumeratedType), requestProperty), - var otherEnumeratedType => GetPopulate(BoxInto(getEnumerable, otherEnumeratedType), requestProperty) + var formattableEnumeratedType when typeof(IFormattable).IsAssignableFrom(formattableEnumeratedType) => GetPopulate(GetEnumerableOf(getEnumerable, formattableEnumeratedType), requestProperty), + var convertibleEnumeratedType when typeof(IConvertible).IsAssignableFrom(convertibleEnumeratedType) => GetPopulate(GetEnumerableOf(getEnumerable, convertibleEnumeratedType), requestProperty), + var otherEnumeratedType => GetPopulate(getEnumerable, requestProperty) }; } @@ -118,8 +121,10 @@ static Action> GetPopulateArray(Func PopulateArray(getEnumerable(entity), toString, newRequestProperty, parameters); } - static Action> GetPopulateArray(Func getEnumerable, RequestProperty requestProperty) => - GetPopulateArray((entity) => getEnumerable(entity).Cast(), requestProperty); + static Action> GetPopulateArray(Func getEnumerable, RequestProperty requestProperty) { + var newRequestProperty = requestProperty with { Name = $"{requestProperty.Name}[]" }; + return (entity, parameters) => PopulateArray(getEnumerable(entity), newRequestProperty, parameters); + } static void Populate(IFormattable formattable, RequestProperty requestProperty, ICollection parameters) => Populate(GetStringValue(formattable, requestProperty), requestProperty, parameters); @@ -141,6 +146,24 @@ static void PopulateCsv(IEnumerable convertibles, RequestProperty static void PopulateCsv(IEnumerable objects, RequestProperty requestProperty, ICollection parameters) => PopulateCsv(objects, @object => GetUnknownStringValue(@object, requestProperty), requestProperty, parameters); + static void PopulateCsv(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { + switch (enumerable) { + case IEnumerable formattables: + PopulateCsv(formattables, requestProperty, parameters); + break; + case IEnumerable convertibles: + PopulateCsv(convertibles, requestProperty, parameters); + break; + case IEnumerable objects: + PopulateCsv(objects, requestProperty, parameters); + break; + default: + PopulateCsvUnknown(enumerable, requestProperty, parameters); + break; + } + } + + static void PopulateCsv(IEnumerable enumerable, Func toString, RequestProperty requestProperty, ICollection parameters) where V : class { const string csvSeparator = ","; var formattedStrings = enumerable.Select(toString); @@ -156,17 +179,8 @@ static void PopulateCsv(object @object, RequestProperty requestProperty, ICollec case IConvertible convertible: Populate(convertible, requestProperty, parameters); break; - case IEnumerable formattables: - PopulateCsv(formattables, requestProperty, parameters); - break; - case IEnumerable convertibles: - PopulateCsv(convertibles, requestProperty, parameters); - break; - case IEnumerable objects: - PopulateCsv(objects, requestProperty, parameters); - break; case IEnumerable enumerable: - PopulateCsvUnknown(enumerable, requestProperty, parameters); + PopulateCsv(enumerable, requestProperty, parameters); break; default: Populate(@object, requestProperty, parameters); @@ -174,9 +188,6 @@ static void PopulateCsv(object @object, RequestProperty requestProperty, ICollec } } - static void PopulateCsvKnown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) => - PopulateCsv(enumerable.Cast(), requestProperty, parameters); - static void PopulateCsvUnknown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { if (GetSingleEnumeratedTypeOrNull(enumerable.GetType()) is not { } enumeratedType) { @@ -197,6 +208,8 @@ static void PopulateCsvUnknown(IEnumerable enumerable, RequestProperty requestPr } } + static void PopulateCsvKnown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) => PopulateCsv(enumerable.Cast(), requestProperty, parameters); + static void PopulateArray(IEnumerable formattables, RequestProperty requestProperty, ICollection parameters) => PopulateArray(formattables, formattable => GetStringValue(formattable, requestProperty), requestProperty, parameters); @@ -206,6 +219,24 @@ static void PopulateArray(IEnumerable convertibles, RequestPropert static void PopulateArray(IEnumerable objects, RequestProperty requestProperty, ICollection parameters) => PopulateArray(objects, @object => GetUnknownStringValue(@object, requestProperty), requestProperty, parameters); + static void PopulateArray(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { + switch (enumerable) { + case IEnumerable formattables: + PopulateArray(formattables, requestProperty, parameters); + break; + case IEnumerable convertibles: + PopulateArray(convertibles, requestProperty, parameters); + break; + case IEnumerable objects: + PopulateArray(objects, requestProperty, parameters); + break; + default: + PopulateArrayUnknown(enumerable, requestProperty, parameters); + break; + } + } + + static void PopulateArray(IEnumerable enumerable, Func toString, RequestProperty requestProperty, ICollection parameters) where V : class { var values = enumerable.Select(toString); @@ -224,20 +255,7 @@ static void PopulateArray(object @object, RequestProperty requestProperty, IColl break; case IEnumerable enumerable: requestProperty = requestProperty with { Name = $"{requestProperty.Name}[]" }; - switch (enumerable) { - case IEnumerable formattables: - PopulateArray(formattables, requestProperty, parameters); - break; - case IEnumerable convertibles: - PopulateArray(convertibles, requestProperty, parameters); - break; - case IEnumerable objects: - PopulateArray(objects, requestProperty, parameters); - break; - default: - PopulateArrayUnknown(enumerable, requestProperty, parameters); - break; - } + PopulateArray(enumerable, requestProperty, parameters); break; default: Populate(@object, requestProperty, parameters); @@ -245,9 +263,6 @@ static void PopulateArray(object @object, RequestProperty requestProperty, IColl } } - static void PopulateArrayKnown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) => - PopulateArray(enumerable.Cast(), requestProperty, parameters); - static void PopulateArrayUnknown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { if (GetSingleEnumeratedTypeOrNull(enumerable.GetType()) is not { } enumeratedType) { @@ -268,16 +283,21 @@ static void PopulateArrayUnknown(IEnumerable enumerable, RequestProperty request } } + static void PopulateArrayKnown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) => PopulateArray(enumerable.Cast(), requestProperty, parameters); + static string GetStringValue(IFormattable formattable, RequestProperty requestProperty) => formattable.ToString(requestProperty.Format, null); + static string GetStringValue(IConvertible convertible) => convertible.ToString(null); + static string? GetKnownStringValue(object @object) => TypeDescriptor.GetConverter(@object).ConvertToString(@object); + static string? GetUnknownStringValue(object @object, RequestProperty requestProperty) => @object switch { IFormattable formattable => GetStringValue(formattable, requestProperty), IConvertible convertible => GetStringValue(convertible), _ => GetKnownStringValue(@object) }; - static Func> BoxInto(Func getEnumerable, Type enumeratedType) where V : class => + static Func> GetEnumerableOf(Func getEnumerable, Type enumeratedType) where V : class => enumeratedType.IsValueType ? entity => getEnumerable(entity).Cast() : entity => Unsafe.As>(getEnumerable(entity))!; From 393aebca0fa509f7c66a11ddebd9f695d627a959 Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Fri, 18 Nov 2022 11:42:40 -0300 Subject: [PATCH 05/12] Clean up code and benchmarks for RestRequestExtensions.AddObjectStatic --- .../AddObjectToRequestParametersBenchmarks.Data.cs | 5 ----- .../AddObjectToRequestParametersBenchmarks.cs | 13 +++++-------- benchmarks/RestSharp.Benchmarks/Requests/Data.cs | 9 +++++++++ ...RestRequestExtensions.PropertyCache.Populator.cs | 3 --- src/RestSharp/Request/RestRequestExtensions.cs | 1 - 5 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.Data.cs create mode 100644 benchmarks/RestSharp.Benchmarks/Requests/Data.cs diff --git a/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.Data.cs b/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.Data.cs deleted file mode 100644 index 6ab91aa43..000000000 --- a/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.Data.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace RestSharp.Benchmarks.Requests { - public partial class AddObjectToRequestParametersBenchmarks { - sealed record Data(string String, int Int32, string[] Strings, int[] Ints); - } -} diff --git a/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs b/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs index edaeccd63..e5174e8df 100644 --- a/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs +++ b/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs @@ -1,11 +1,11 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; +using System.Globalization; namespace RestSharp.Benchmarks.Requests { [MemoryDiagnoser, RankColumn, Orderer(SummaryOrderPolicy.FastestToSlowest)] public partial class AddObjectToRequestParametersBenchmarks { Data _data; - string[] _fields; [GlobalSetup] public void GlobalSetup() { @@ -16,8 +16,10 @@ public void GlobalSetup() { var ints = new int[arraySize]; Array.Fill(ints, int.MaxValue); - _data = new Data(@string, int.MaxValue, strings, ints); - _fields = new[] { nameof(Data.String), nameof(Data.Int32), nameof(Data.Strings), nameof(Data.Ints) }; + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + var dateTime = DateTime.Parse("01/01/2013 03:03:12"); + + _data = new Data(@string, int.MaxValue, strings, ints, dateTime, strings); } [Benchmark(Baseline = true)] @@ -26,10 +28,5 @@ public void GlobalSetup() { [Benchmark] public void AddObjectStatic() => new RestRequest().AddObjectStatic(_data); - [Benchmark] - public void AddObject_Filtered() => new RestRequest().AddObject(_data, _fields); - - [Benchmark] - public void AddObjectStatic_Filtered() => new RestRequest().AddObjectStatic(_data, _fields); } } diff --git a/benchmarks/RestSharp.Benchmarks/Requests/Data.cs b/benchmarks/RestSharp.Benchmarks/Requests/Data.cs new file mode 100644 index 000000000..350d5376b --- /dev/null +++ b/benchmarks/RestSharp.Benchmarks/Requests/Data.cs @@ -0,0 +1,9 @@ +namespace RestSharp.Benchmarks.Requests { + sealed record Data( + string String, + [property: RequestProperty(Name = "PropertyName")] int Int32, + string[] Strings, + [property: RequestProperty(Format = "00000", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] int[] Ints, + [property: RequestProperty(Name = "DateTime", Format = "hh:mm tt")] object DateTime, + object StringArray); +} diff --git a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs index c1719e5a9..38f76248f 100644 --- a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs @@ -113,9 +113,6 @@ static Action> GetPopulateArray(Func> GetPopulateArray(Func> getConvertibles, RequestProperty requestProperty) => GetPopulateArray(getConvertibles, GetStringValue, requestProperty); - static Action> GetPopulateArray(Func> getObjects, RequestProperty requestProperty) => - GetPopulateArray(getObjects, @object => GetUnknownStringValue(@object, requestProperty), requestProperty); - static Action> GetPopulateArray(Func> getEnumerable, Func toString, RequestProperty requestProperty) where V : class { var newRequestProperty = requestProperty with { Name = $"{requestProperty.Name}[]" }; return (entity, parameters) => PopulateArray(getEnumerable(entity), toString, newRequestProperty, parameters); diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index e5e213147..bc7416e29 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -14,7 +14,6 @@ using System.Net; using System.Text.RegularExpressions; -using RestSharp.Extensions; using RestSharp.Serializers; namespace RestSharp; From 3cb3493f56ed9b2dc0d3fecb62f847d369093fc4 Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Fri, 18 Nov 2022 16:24:48 -0300 Subject: [PATCH 06/12] Ensure ref structs are not taken into account for RestRequestExtensions.AddObjectStatic --- .../RestRequestExtensions.PropertyCache.cs | 5 +++++ test/RestSharp.Tests/ObjectParameterTests.cs | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs index 9f20f2569..95c99cd55 100644 --- a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs @@ -21,6 +21,11 @@ static partial class PropertyCache where T : class { static readonly IReadOnlyCollection Populators = typeof(T) .GetProperties(BindingFlags.Public | BindingFlags.Instance) +#if NETCOREAPP2_1_OR_GREATER + .Where(property => !property.PropertyType.IsByRefLike) +#else + .Where(property => !property.PropertyType.IsDefined(Type.GetType("System.Runtime.CompilerServices.IsByRefLikeAttribute"))) +#endif .Select(Populator.From) .ToArray(); diff --git a/test/RestSharp.Tests/ObjectParameterTests.cs b/test/RestSharp.Tests/ObjectParameterTests.cs index 6a69f7ba3..3d1035fc1 100644 --- a/test/RestSharp.Tests/ObjectParameterTests.cs +++ b/test/RestSharp.Tests/ObjectParameterTests.cs @@ -698,4 +698,22 @@ public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_as_Object_usin new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", ".NET"), }); } + + [Fact] + public void RefStructs_are_ignored() { + const string value = "Hello world"; + var stringValue = new StringValue(value); + var request = new RestRequest().AddObjectStatic(stringValue); + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(StringValue.Value), value)); + } + + public sealed record StringValue(string Value) { + public ReadOnlySpan AsSpan => Value.AsSpan(); + } } From d88fb1543b5d4d88447cd4cdbfd5f98078a0a98f Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Fri, 18 Nov 2022 16:43:39 -0300 Subject: [PATCH 07/12] Specify RestRequestExtensions.AddObjectStatic CSV separator based on target framework version --- .../Request/RestRequestExtensions.PropertyCache.Populator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs index 38f76248f..ca9dfe57f 100644 --- a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs @@ -162,7 +162,11 @@ static void PopulateCsv(IEnumerable enumerable, RequestProperty requestProperty, static void PopulateCsv(IEnumerable enumerable, Func toString, RequestProperty requestProperty, ICollection parameters) where V : class { +#if NETCOREAPP2_0_OR_GREATER + const char csvSeparator = ','; +#else const string csvSeparator = ","; +#endif var formattedStrings = enumerable.Select(toString); var csv = string.Join(csvSeparator, formattedStrings); Populate(csv, requestProperty, parameters); From 57e0e1d450ccf3d142f97125798ee353c88ea951 Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Fri, 18 Nov 2022 18:43:09 -0300 Subject: [PATCH 08/12] Add documentation to changes regarding RestRequestExtensions.AddObjectStatic --- ...questExtensions.PropertyCache.Populator.cs | 77 ++++++++++++++++--- .../RestRequestExtensions.PropertyCache.cs | 20 +++++ .../Request/RestRequestExtensions.cs | 27 ++++++- 3 files changed, 112 insertions(+), 12 deletions(-) diff --git a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs index ca9dfe57f..6e840c174 100644 --- a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs @@ -23,6 +23,13 @@ namespace RestSharp; public static partial class RestRequestExtensions { static partial class PropertyCache where T : class { sealed partial class Populator { + /// + /// Gets the name of the property this populator represents. + /// + /// + /// This corresponds to the actual property name and not the name + /// determined by + /// internal string PropertyName { get; } readonly Action> _populate; @@ -31,17 +38,35 @@ private Populator(string propertyName, Action> populat _populate = populate; } + /// + /// Populates the provided parameters collection + /// + /// The object to get parameters from + /// The parameters collection to populate internal void Populate(T entity, ICollection parameters) => _populate(entity, parameters); + /// + /// Creates a new populator instance from the provided property + /// + /// A public instance property from the type + /// internal static Populator From(PropertyInfo property) { var entity = Expression.Parameter(typeof(T)); var callGetter = Expression.Call(entity, property.GetGetMethod()!); Expression convertGetterReturnToObject = property.PropertyType.IsValueType ? + // Values types are not automatically boxed in LINQ expressions. + // This would throw an exception. Expression.Convert(callGetter, typeof(object)) : + // Avoid unnecessary cast to object if property is already a reference type. callGetter; + // This compiles roughly to: `(T entity) => (object)entity.get_Property()`, + // where `.GetProperty()` is the getter. The reason we use LINQ expressions + // instead of direct calls to the MethodInfo instance is for an increase in + // performance. We can then leverage our knowledge of the type parameter provided. + var getObject = Expression.Lambda>(convertGetterReturnToObject, entity).Compile(); var populate = GetPopulate(getObject, property); @@ -57,33 +82,40 @@ internal static Populator From(PropertyInfo property) { RequestArrayQueryType.CommaSeparated => (model, parameters) => PopulateCsv(getFormattables(model), requestProperty, parameters), RequestArrayQueryType.ArrayParameters => GetPopulateArray(getFormattables, requestProperty), _ => (_, _) => { } - }; + }; // Here we avoid the cost of checking if the format is CSV or Array every time by caching the result of this evaluation. static Action> GetPopulate(Func> getConvertibles, RequestProperty requestProperty) => requestProperty.ArrayQueryType switch { RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsv(getConvertibles(entity), requestProperty, parameters), RequestArrayQueryType.ArrayParameters => GetPopulateArray(getConvertibles, requestProperty), _ => (_, _) => { } - }; + }; // Here we avoid the cost of checking if the format is CSV or Array every time by caching the result of this evaluation. static Action> GetPopulate(Func getEnumerable, RequestProperty requestProperty) => requestProperty.ArrayQueryType switch { RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsv(getEnumerable(entity), requestProperty, parameters), RequestArrayQueryType.ArrayParameters => GetPopulateArray(getEnumerable, requestProperty), _ => (_, _) => { } - }; + }; // Here we avoid the cost of checking if the format is CSV or Array every time by caching the result of this evaluation. static Action> GetPopulate(Func getObject, RequestProperty requestProperty) => requestProperty.ArrayQueryType switch { RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsv(getObject(entity), requestProperty, parameters), RequestArrayQueryType.ArrayParameters => (entity, parameters) => PopulateArray(getObject(entity), requestProperty, parameters), _ => (_, _) => { } - }; + }; // Here we avoid the cost of checking if the format is CSV or Array every time by caching the result of this evaluation. static Action> GetPopulate(Func getObject, PropertyInfo property) { var requestProperty = RequestProperty.From(property); + // We need to use different conversion mechanisms for each return type. Simply calling `.ToString()` + // on every returned object would not take into account special cases like custom formatting, enumeration etc. + // Unchecked casts here are safe because we know the return type of `getObject` is boxed if needed. return property.PropertyType switch { var formattableType when typeof(IFormattable).IsAssignableFrom(formattableType) => GetPopulate(entity => Unsafe.As(getObject(entity))!, requestProperty), var convertibleType when typeof(IConvertible).IsAssignableFrom(convertibleType) => GetPopulate(entity => Unsafe.As(getObject(entity))!, requestProperty), var enumerableType when typeof(IEnumerable).IsAssignableFrom(enumerableType) => GetPopulateUnknown(entity => Unsafe.As(getObject(entity))!, requestProperty), + // At this point we're not necessarily sure we can just treat this as a bare object + // and use its type converter. Even though the property itself returns an object, + // the object returned itself may need to be treated in a special way, so we check + // it as we go. var otherType => GetPopulate(getObject, requestProperty) }; } @@ -91,12 +123,18 @@ var enumerableType when typeof(IEnumerable).IsAssignableFrom(enumerableType) => static Action> GetPopulateUnknown(Func getEnumerable, RequestProperty requestProperty) { if (GetSingleEnumeratedTypeOrNull(requestProperty.Type) is not { } enumeratedType) { + // Means we're dealing with a legacy, untyped enumerable instance. + // We can just convert it into an enumerable of objects and delegate + // conversion to string to the type converter of each enumerated item. return GetPopulateKnown(getEnumerable, requestProperty); } return enumeratedType switch { var formattableEnumeratedType when typeof(IFormattable).IsAssignableFrom(formattableEnumeratedType) => GetPopulate(GetEnumerableOf(getEnumerable, formattableEnumeratedType), requestProperty), var convertibleEnumeratedType when typeof(IConvertible).IsAssignableFrom(convertibleEnumeratedType) => GetPopulate(GetEnumerableOf(getEnumerable, convertibleEnumeratedType), requestProperty), + // At this point we're not necessarily sure we can just treat this as an enumerable of objects. + // Since we know the actual enumerable may be a typed `IEnumerable<>` enumerating a type we're + // interested in, we do further checks to ensure the correct conversion to string is applied. var otherEnumeratedType => GetPopulate(getEnumerable, requestProperty) }; } @@ -105,7 +143,7 @@ var convertibleEnumeratedType when typeof(IConvertible).IsAssignableFrom(convert RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsvUnknown(getEnumerable(entity), requestProperty, parameters), RequestArrayQueryType.ArrayParameters => GetPopulateArray(getEnumerable, requestProperty), _ => (_, _) => { } - }; + }; // Here we avoid the cost of checking if the format is CSV or Array every time by caching the result of this evaluation. static Action> GetPopulateArray(Func> getFormattables, RequestProperty requestProperty) => GetPopulateArray(getFormattables, formattable => GetStringValue(formattable, requestProperty), requestProperty); @@ -114,11 +152,13 @@ static Action> GetPopulateArray(Func> GetPopulateArray(Func> getEnumerable, Func toString, RequestProperty requestProperty) where V : class { + // We do this to avoid recreating request property on each iteration. var newRequestProperty = requestProperty with { Name = $"{requestProperty.Name}[]" }; return (entity, parameters) => PopulateArray(getEnumerable(entity), toString, newRequestProperty, parameters); } static Action> GetPopulateArray(Func getEnumerable, RequestProperty requestProperty) { + // We do this to avoid recreating request property on each iteration. var newRequestProperty = requestProperty with { Name = $"{requestProperty.Name}[]" }; return (entity, parameters) => PopulateArray(getEnumerable(entity), newRequestProperty, parameters); } @@ -127,7 +167,7 @@ static Action> GetPopulateArray(Func g static void Populate(IConvertible convertible, RequestProperty requestProperty, ICollection parameters) => Populate(GetStringValue(convertible), requestProperty, parameters); - static void Populate(object @object, RequestProperty requestProperty, ICollection parameters) => Populate(GetKnownStringValue(@object), requestProperty, parameters); + static void Populate(object @object, RequestProperty requestProperty, ICollection parameters) => Populate(GetStringValueKnown(@object), requestProperty, parameters); static void Populate(string? stringValue, RequestProperty requestProperty, ICollection parameters) { var parameter = new GetOrPostParameter(requestProperty.Name, stringValue); @@ -141,7 +181,7 @@ static void PopulateCsv(IEnumerable convertibles, RequestProperty PopulateCsv(convertibles, GetStringValue, requestProperty, parameters); static void PopulateCsv(IEnumerable objects, RequestProperty requestProperty, ICollection parameters) => - PopulateCsv(objects, @object => GetUnknownStringValue(@object, requestProperty), requestProperty, parameters); + PopulateCsv(objects, @object => GetStringValueUnknown(@object, requestProperty), requestProperty, parameters); static void PopulateCsv(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { switch (enumerable) { @@ -184,6 +224,8 @@ static void PopulateCsv(object @object, RequestProperty requestProperty, ICollec PopulateCsv(enumerable, requestProperty, parameters); break; default: + // At this point it's safe to assume we can delegate + // to the type converter. Populate(@object, requestProperty, parameters); break; } @@ -192,6 +234,9 @@ static void PopulateCsv(object @object, RequestProperty requestProperty, ICollec static void PopulateCsvUnknown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { if (GetSingleEnumeratedTypeOrNull(enumerable.GetType()) is not { } enumeratedType) { + // Means we're dealing with a legacy, untyped enumerable instance. + // We can just convert it into an enumerable of objects and delegate + // conversion to string to the type converter of each enumerated item. PopulateCsvKnown(enumerable, requestProperty, parameters); return; } @@ -218,7 +263,7 @@ static void PopulateArray(IEnumerable convertibles, RequestPropert PopulateArray(convertibles, GetStringValue, requestProperty, parameters); static void PopulateArray(IEnumerable objects, RequestProperty requestProperty, ICollection parameters) => - PopulateArray(objects, @object => GetUnknownStringValue(@object, requestProperty), requestProperty, parameters); + PopulateArray(objects, @object => GetStringValueUnknown(@object, requestProperty), requestProperty, parameters); static void PopulateArray(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { switch (enumerable) { @@ -255,10 +300,13 @@ static void PopulateArray(object @object, RequestProperty requestProperty, IColl Populate(convertible, requestProperty, parameters); break; case IEnumerable enumerable: + // We do this to avoid recreating request property on each iteration. requestProperty = requestProperty with { Name = $"{requestProperty.Name}[]" }; PopulateArray(enumerable, requestProperty, parameters); break; default: + // At this point it's safe to assume we can delegate + // to the type converter. Populate(@object, requestProperty, parameters); break; } @@ -267,6 +315,9 @@ static void PopulateArray(object @object, RequestProperty requestProperty, IColl static void PopulateArrayUnknown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { if (GetSingleEnumeratedTypeOrNull(enumerable.GetType()) is not { } enumeratedType) { + // Means we're dealing with a legacy, untyped enumerable instance. + // We can just convert it into an enumerable of objects and delegate + // conversion to string to the type converter of each enumerated item. PopulateArrayKnown(enumerable, requestProperty, parameters); return; } @@ -290,12 +341,12 @@ static void PopulateArrayUnknown(IEnumerable enumerable, RequestProperty request static string GetStringValue(IConvertible convertible) => convertible.ToString(null); - static string? GetKnownStringValue(object @object) => TypeDescriptor.GetConverter(@object).ConvertToString(@object); + static string? GetStringValueKnown(object @object) => TypeDescriptor.GetConverter(@object).ConvertToString(@object); - static string? GetUnknownStringValue(object @object, RequestProperty requestProperty) => @object switch { + static string? GetStringValueUnknown(object @object, RequestProperty requestProperty) => @object switch { IFormattable formattable => GetStringValue(formattable, requestProperty), IConvertible convertible => GetStringValue(convertible), - _ => GetKnownStringValue(@object) + _ => GetStringValueKnown(@object) }; static Func> GetEnumerableOf(Func getEnumerable, Type enumeratedType) where V : class => @@ -304,6 +355,7 @@ static Func> GetEnumerableOf(Func getEnumer entity => Unsafe.As>(getEnumerable(entity))!; static Type? GetSingleEnumeratedTypeOrNull(Type enumerableType) { + // Get all IEnumerable<> interfaces this type implements. var enumerableInterfaces = enumerableType .GetInterfaces() @@ -311,6 +363,9 @@ static Func> GetEnumerableOf(Func getEnumer .Where(@interface => @interface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) .ToArray(); + // If this type implements `IEnumerable<>` multiple times with different type parameters + // we cannot pick which implementation to "believe", so we treat the whole thing as a bare, + // untyped `IEnumerable`. return enumerableInterfaces.Length == 1 ? enumerableInterfaces[0].GetGenericArguments()[0] : null; } } diff --git a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs index 95c99cd55..b53c1137d 100644 --- a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs @@ -21,6 +21,9 @@ static partial class PropertyCache where T : class { static readonly IReadOnlyCollection Populators = typeof(T) .GetProperties(BindingFlags.Public | BindingFlags.Instance) + // We need to ensure the property does not return a ref struct + // since reflection and LINQ expressions do not play well with + // them. All bets are off, so let's just ignore them. #if NETCOREAPP2_1_OR_GREATER .Where(property => !property.PropertyType.IsByRefLike) #else @@ -29,6 +32,12 @@ static partial class PropertyCache where T : class { .Select(Populator.From) .ToArray(); + /// + /// Gets parameters from the provided object + /// + /// The object from which to get the parameters + /// Properties to include, or nothing to include everything. The array will be sorted. + /// internal static IEnumerable GetParameters(T entity, params string[] includedProperties) { if (includedProperties.Length == 0) { return GetParameters(entity); @@ -36,15 +45,26 @@ internal static IEnumerable GetParameters(T entity, params string[] i Array.Sort(includedProperties); // Otherwise binary search is unsafe. + // Get only populators found in `includedProperties`. + var populators = Populators.Where(populator => Array.BinarySearch(includedProperties, populator.PropertyName) >= 0); return GetParameters(entity, populators); } + + /// + /// Gets parameters from the provided object + /// + /// The object from which to get the parameters + /// internal static IEnumerable GetParameters(T entity) => GetParameters(entity, Populators); static IEnumerable GetParameters(T entity, IEnumerable populators) { var parameters = new List(capacity: Populators.Count); foreach (var populator in populators) { + // Each populator may return one or more parameters, + // so they take temporary ownership of the list in order + // to populate it with its own set of parameters. populator.Populate(entity, parameters); } diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index bc7416e29..7a41296e5 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -438,7 +438,7 @@ public static RestRequest AddXmlBody(this RestRequest request, T obj, string /// /// Request instance /// Object to add as form data - /// Properties to include, or nothing to include everything + /// Properties to include, or nothing to include everything. The array will be sorted. /// public static RestRequest AddObject(this RestRequest request, T obj, params string[] includedProperties) where T : class { var props = obj.GetProperties(includedProperties); @@ -450,9 +450,34 @@ public static RestRequest AddObject(this RestRequest request, T obj, params s return request; } + /// + /// Gets object properties and adds each property as a form data parameter + /// + /// + /// This method gets public instance properties from the provided type + /// rather than from itself, which allows for caching of properties and + /// other optimizations. If you don't know the type at runtime, or wish to use properties not + /// available from the provided type parameter, consider using + /// + /// Request instance + /// Object to add as form data + /// Properties to include, or nothing to include everything. The array will be sorted. + /// public static RestRequest AddObjectStatic(this RestRequest request, T obj, params string[] includedProperties) where T : class => request.AddParameters(PropertyCache.GetParameters(obj, includedProperties)); + /// + /// Gets object properties and adds each property as a form data parameter + /// + /// + /// This method gets public instance properties from the provided type + /// rather than from itself, which allows for caching of properties and + /// other optimizations. If you don't know the type at runtime, or wish to use properties not + /// available from the provided type parameter, consider using + /// + /// Request instance + /// Object to add as form data + /// public static RestRequest AddObjectStatic(this RestRequest request, T obj) where T : class => request.AddParameters(PropertyCache.GetParameters(obj)); From 155a77f0196e156cf2b9c33b1cbf53d71fbcbce5 Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Mon, 21 Nov 2022 18:25:39 -0300 Subject: [PATCH 09/12] Fix bug that would allow attempt to turn ref structs into query parameters --- src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs index b53c1137d..3e9b7d4ee 100644 --- a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs @@ -27,7 +27,10 @@ static partial class PropertyCache where T : class { #if NETCOREAPP2_1_OR_GREATER .Where(property => !property.PropertyType.IsByRefLike) #else - .Where(property => !property.PropertyType.IsDefined(Type.GetType("System.Runtime.CompilerServices.IsByRefLikeAttribute"))) + // Since `IsByRefLikeAttribute` is generated at compile time, each assembly + // may have its own definition of the attribute, so we must compare by full name + // instead of type. + .Where(property => !property.PropertyType.GetCustomAttributes().Select(attribute => attribute.GetType().FullName).Any(attributeName => attributeName == "System.Runtime.CompilerServices.IsByRefLikeAttribute")) #endif .Select(Populator.From) .ToArray(); From 361351fc5855a06f5c9108fe77f420c42f4b2325 Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Mon, 21 Nov 2022 18:37:46 -0300 Subject: [PATCH 10/12] Add test case to ensure RestRequestExtensions.AddObjectStatic filters properties correctly --- test/RestSharp.Tests/ObjectParameterTests.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/RestSharp.Tests/ObjectParameterTests.cs b/test/RestSharp.Tests/ObjectParameterTests.cs index 3d1035fc1..b67c49510 100644 --- a/test/RestSharp.Tests/ObjectParameterTests.cs +++ b/test/RestSharp.Tests/ObjectParameterTests.cs @@ -713,6 +713,20 @@ public void RefStructs_are_ignored() { .BeEquivalentTo(new GetOrPostParameter(nameof(StringValue.Value), value)); } + [Fact] + public void Properties_are_filtered() { + var @object = new { Name = "Hello world", Age = 12, Guid = Guid.Parse("72df165c-0cef-4654-987f-cd844f1e5ce9"), Ignore = "Ignored" }; + var request = new RestRequest().AddObjectStatic(@object, nameof(@object.Name), nameof(@object.Age), nameof(@object.Guid)); + request + .Parameters + .Should() + .BeEquivalentTo(new[] { + new GetOrPostParameter(nameof(@object.Name), "Hello world"), + new GetOrPostParameter(nameof(@object.Age), "12"), + new GetOrPostParameter(nameof(@object.Guid), "72df165c-0cef-4654-987f-cd844f1e5ce9") + }); + } + public sealed record StringValue(string Value) { public ReadOnlySpan AsSpan => Value.AsSpan(); } From d5c4a78b46393e7dab28e845c7724a1e6cb13c28 Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Mon, 21 Nov 2022 18:57:38 -0300 Subject: [PATCH 11/12] Clean up code --- .../Extensions/ReflectionExtensions.cs | 8 +++--- src/RestSharp/Parameters/ObjectParser.cs | 10 +++---- .../Request/RestRequestExtensions.cs | 26 +++++++++---------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/RestSharp/Extensions/ReflectionExtensions.cs b/src/RestSharp/Extensions/ReflectionExtensions.cs index 5977de18a..a16513535 100644 --- a/src/RestSharp/Extensions/ReflectionExtensions.cs +++ b/src/RestSharp/Extensions/ReflectionExtensions.cs @@ -1,11 +1,11 @@ // Copyright (c) .NET Foundation and Contributors -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,7 +15,7 @@ using System.Globalization; using System.Reflection; -namespace RestSharp.Extensions; +namespace RestSharp.Extensions; /// /// Reflection extensions diff --git a/src/RestSharp/Parameters/ObjectParser.cs b/src/RestSharp/Parameters/ObjectParser.cs index 52ee7e8cf..71e496798 100644 --- a/src/RestSharp/Parameters/ObjectParser.cs +++ b/src/RestSharp/Parameters/ObjectParser.cs @@ -1,17 +1,17 @@ // Copyright (c) .NET Foundation and Contributors -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// +// using System.Reflection; @@ -58,7 +58,7 @@ static class ObjectParser { RequestArrayQueryType.ArrayParameters => values.Select(x => ($"{name}[]", x)), _ => throw new ArgumentOutOfRangeException() }; - + } return new (string, string?)[] { (name, null) }; diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index 7a41296e5..a886069c1 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -1,11 +1,11 @@ // Copyright (c) .NET Foundation and Contributors -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -481,16 +481,16 @@ public static RestRequest AddObjectStatic(this RestRequest request, T obj, pa public static RestRequest AddObjectStatic(this RestRequest request, T obj) where T : class => request.AddParameters(PropertyCache.GetParameters(obj)); - /// - /// Adds cookie to the cookie container. - /// - /// RestRequest to add the cookies to - /// Cookie name - /// Cookie value - /// Cookie path - /// Cookie domain, must not be an empty string - /// - public static RestRequest AddCookie(this RestRequest request, string name, string value, string path, string domain) { + /// + /// Adds cookie to the cookie container. + /// + /// RestRequest to add the cookies to + /// Cookie name + /// Cookie value + /// Cookie path + /// Cookie domain, must not be an empty string + /// + public static RestRequest AddCookie(this RestRequest request, string name, string value, string path, string domain) { request.CookieContainer ??= new CookieContainer(); request.CookieContainer.Add(new Cookie(name, value, path, domain)); return request; From d8b1fc3e2da804ff4eaa80ecd37a8cee6675575a Mon Sep 17 00:00:00 2001 From: Abner Ferreira Date: Mon, 21 Nov 2022 19:09:56 -0300 Subject: [PATCH 12/12] Add missing documentation to internal properties and methods in RestRequestExtensions.PropertyCache.Populator.RequestProperty --- ...PropertyCache.Populator.RequestProperty.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.RequestProperty.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.RequestProperty.cs index def6aeb2c..6426a2e1d 100644 --- a/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.RequestProperty.cs +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.RequestProperty.cs @@ -20,9 +20,24 @@ public static partial class RestRequestExtensions { static partial class PropertyCache where T : class { sealed partial class Populator { sealed record RequestProperty { + /// + /// Gets or sets the associated + /// with the property this object represents + /// internal string Name { get; init; } + /// + /// Gets the associated with + /// the property this object represents + /// internal string? Format { get; } + /// + /// Gets the associated + /// with the property this object represents + /// internal RequestArrayQueryType ArrayQueryType { get; } + /// + /// Gets the return type of the property this object represents + /// internal Type Type { get; } private RequestProperty(string name, string? format, RequestArrayQueryType arrayQueryType, Type type) { @@ -32,6 +47,11 @@ private RequestProperty(string name, string? format, RequestArrayQueryType array Type = type; } + /// + /// Creates a new request property representation of the provided property + /// + /// The property to turn into a request property + /// internal static RequestProperty From(PropertyInfo property) { var requestPropertyAttribute = property.GetCustomAttribute() ??