diff --git a/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs b/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs new file mode 100644 index 000000000..e5174e8df --- /dev/null +++ b/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs @@ -0,0 +1,32 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using System.Globalization; + +namespace RestSharp.Benchmarks.Requests { + [MemoryDiagnoser, RankColumn, Orderer(SummaryOrderPolicy.FastestToSlowest)] + public partial class AddObjectToRequestParametersBenchmarks { + Data _data; + + [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); + + 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)] + public void AddObject() => new RestRequest().AddObject(_data); + + [Benchmark] + public void AddObjectStatic() => new RestRequest().AddObjectStatic(_data); + + } +} 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.RequestProperty.cs b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.RequestProperty.cs new file mode 100644 index 000000000..6426a2e1d --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.RequestProperty.cs @@ -0,0 +1,72 @@ +// 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 { + /// + /// 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) { + Name = name; + Format = format; + ArrayQueryType = arrayQueryType; + 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() ?? + 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..6e840c174 --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.Populator.cs @@ -0,0 +1,373 @@ +// 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 { + /// + /// 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; + + private Populator(string propertyName, Action> populate) { + PropertyName = propertyName; + _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); + + 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), + _ => (_, _) => { } + }; // 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) + }; + } + + 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) + }; + } + + static Action> GetPopulateKnown(Func getEnumerable, RequestProperty requestProperty) => requestProperty.ArrayQueryType switch { + 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); + + static Action> GetPopulateArray(Func> getConvertibles, RequestProperty requestProperty) => + GetPopulateArray(getConvertibles, GetStringValue, requestProperty); + + static Action> 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); + } + + 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(GetStringValueKnown(@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 => GetStringValueUnknown(@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 { +#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); + } + + 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 enumerable: + 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; + } + } + + 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; + } + + 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 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); + + 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 => GetStringValueUnknown(@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); + + 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: + // 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; + } + } + + 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; + } + + 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 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? GetStringValueKnown(object @object) => TypeDescriptor.GetConverter(@object).ConvertToString(@object); + + static string? GetStringValueUnknown(object @object, RequestProperty requestProperty) => @object switch { + IFormattable formattable => GetStringValue(formattable, requestProperty), + IConvertible convertible => GetStringValue(convertible), + _ => GetStringValueKnown(@object) + }; + + static Func> GetEnumerableOf(Func getEnumerable, Type enumeratedType) where V : class => + enumeratedType.IsValueType ? + entity => getEnumerable(entity).Cast() : + entity => Unsafe.As>(getEnumerable(entity))!; + + static Type? GetSingleEnumeratedTypeOrNull(Type enumerableType) { + // Get all IEnumerable<> interfaces this type implements. + var enumerableInterfaces = + enumerableType + .GetInterfaces() + .Where(@interface => @interface.IsGenericType) + .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 new file mode 100644 index 000000000..3e9b7d4ee --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.PropertyCache.cs @@ -0,0 +1,77 @@ +// 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) + // 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 + // 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(); + + /// + /// 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); + } + + 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); + } + + return parameters; + } + } +} diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index a35da5621..a886069c1 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -14,13 +14,12 @@ using System.Net; using System.Text.RegularExpressions; -using RestSharp.Extensions; using RestSharp.Serializers; namespace RestSharp; [PublicAPI] -public static class RestRequestExtensions { +public static partial class RestRequestExtensions { static readonly Regex PortSplitRegex = new(@":\d+"); /// @@ -45,6 +44,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 +334,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 @@ -434,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); @@ -446,6 +450,37 @@ 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)); + /// /// 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.cs b/test/RestSharp.Tests/ObjectParameterTests.cs index 2be042e6b..b67c49510 100644 --- a/test/RestSharp.Tests/ObjectParameterTests.cs +++ b/test/RestSharp.Tests/ObjectParameterTests.cs @@ -1,11 +1,733 @@ +using System.Collections; +using System.Globalization; + namespace RestSharp.Tests; -public class ObjectParameterTests { +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 }; + 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 + + [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"), + }); + } + + [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)); + } + + [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(); + } +}