diff --git a/RestSharp.sln.DotSettings b/RestSharp.sln.DotSettings index 975adf68e..0d8bf09d0 100644 --- a/RestSharp.sln.DotSettings +++ b/RestSharp.sln.DotSettings @@ -82,6 +82,7 @@ True True True + True True True True diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index c6dc9d4cf..807d72662 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; using System.Text.RegularExpressions; -using RestSharp.Extensions; using RestSharp.Serializers; namespace RestSharp; @@ -208,6 +210,25 @@ public static RestRequest AddParameter(this RestRequest request, string? name, o ? request.AddBodyParameter(name, value) : request.AddParameter(Parameter.CreateParameter(name, value, type, encode)); + /// + /// Adds the provided parameters container to the request. + /// + /// + /// The provided model has its public properties interpreted as parameters of the provided . + /// + /// Request instance + /// An arbitrary model containing public properties that should be interpreted as parameters of the provided type + /// Enum value specifying what kind of parameters are being added + /// Encode the value or not, default true + /// The type of the arbitrary model being provided as a parameters container. This information is needed in order to + /// cache properties based on their static type, for performance reasons. + /// The provided instance updated. + public static RestRequest AddParameters(this RestRequest request, TParams @params, ParameterType type, bool encode = true) + where TParams : class { + request.Parameters.AddParameters(Parameterizer.GetParameters(@params, type, encode)); + return request; + } + static RestRequest AddBodyParameter(this RestRequest request, string? name, object value) => name != null && name.Contains("/") ? request.AddBody(value, name) @@ -432,4 +453,98 @@ static void CheckAndThrowsDuplicateKeys(ICollection if (duplicateKeys.Any()) throw new ArgumentException($"Duplicate header names exist: {string.Join(", ", duplicateKeys)}"); } + + static class Parameterizer where TParams : class { + static readonly IReadOnlyCollection Properties = + typeof(TParams).GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(ParameterizedProperty.GetParameterizedPublicInstanceProperty) + .ToArray(); + + /// + /// Instantiates and gets a new instance containing the values of the public properties of the provided + /// as its underlying parameters. + /// + /// An arbitrary model containing public properties that should be interpreted as parameters of the provided type + /// Enum value specifying what kind of parameter is being added + /// Encode the value or not, default true + /// A new parameters collection containing the values of the public properties of the provided model as its underlying + /// parameters + internal static ParametersCollection GetParameters(TParams @params, ParameterType type, bool encode = true) { + var parameters = Properties.Select( + param => Parameter.CreateParameter( + param.Name, + param.GetPropertyValue(@params), + type, + encode + ) + ); + return new ParametersCollection(parameters); + } + + sealed record ParameterizedProperty { + /// + /// Gets the of the property from which this + /// instance was created. + /// + internal string Name { get; } + readonly Func _getPropertyValue; + + ParameterizedProperty(string name, Func getPropertyValue) { + Name = name; + _getPropertyValue = getPropertyValue; + } + + /// + /// Gets the value of the property this instance represents for the provided model. + /// + /// The parameters container from which to get the value of the property this instance represents. + /// The value of the property this instance represents for the specified parameters container. + internal string GetPropertyValue(TParams @params) => _getPropertyValue(@params); + + /// + /// Returns a new instance from the provided . + /// otherwise returns null. + /// + /// + /// The provided getter must be known not to be null before calling this method. It must also be an instance + /// property, as opposed to a static one. + /// + /// The property from which to try creating a new parameterized property. + /// A new parameterized property caching the information of the provided instance. + internal static ParameterizedProperty GetParameterizedPublicInstanceProperty(PropertyInfo property) + => new ParameterizedProperty(property.Name, MakeGetPropertyValue(property.GetGetMethod())); + + static Func MakeGetPropertyValue(MethodInfo getter) { + var paramsParam = Expression.Parameter(typeof(TParams)); + + Expression callGetter = Expression.Call(paramsParam, getter); + var convertToStringExpression = GetConvertToStringExpression(); + + Expression GetConvertToStringExpression() { + var getterReturnType = getter.ReturnType; + + if (getterReturnType == typeof(string)) { + return callGetter; + } + + var toStringConverter = TypeDescriptor.GetConverter(getterReturnType); + + var convertToStringMethod = typeof(TypeConverter).GetMethod( + nameof(TypeConverter.ConvertToInvariantString), + new[] { getterReturnType } + )!; + + var convertToStringMethodFirstParamType = convertToStringMethod.GetParameters().First().ParameterType; + + if (getterReturnType.IsValueType && !convertToStringMethodFirstParamType.IsValueType) { + callGetter = Expression.Convert(callGetter, convertToStringMethodFirstParamType); + } + + return Expression.Call(Expression.Constant(toStringConverter), convertToStringMethod, callGetter); + } + + return Expression.Lambda>(convertToStringExpression, paramsParam).Compile(); + } + } + } } diff --git a/test/RestSharp.Tests/RestRequestExtensionsTests.cs b/test/RestSharp.Tests/RestRequestExtensionsTests.cs new file mode 100644 index 000000000..5bbc71af5 --- /dev/null +++ b/test/RestSharp.Tests/RestRequestExtensionsTests.cs @@ -0,0 +1,48 @@ +// Copyright © 2009-2020 John Sheehan, Andrew Young, Alexey Zimarev and RestSharp community +// +// 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. +// + +#nullable enable +using System.ComponentModel; + +namespace RestSharp.Tests; + +public sealed class RestRequestExtensionsTests { + [Fact] + public void RestRequest_AddParameters_AddsParameters() { + var model = new User(Guid.Parse("27b7acdd-6184-4e21-9b64-cdaa2b2477bd"), "Joe", null, DateTime.Parse("2022-01-01T00:00:00Z"), 100, 100, 100); + var request = new RestRequest().AddParameters(model, ParameterType.QueryString); + + string ConvertToInvariantString(object value) => TypeDescriptor.GetConverter(value.GetType()).ConvertToInvariantString(value); + + var expectedParameters = new ParametersCollection( + new[] { + new QueryParameter(nameof(User.Id), ConvertToInvariantString(model.Id)), + new QueryParameter(nameof(User.FirstName), model.FirstName), + new QueryParameter(nameof(User.LastName), model.LastName), + new QueryParameter(nameof(User.LastLogin), ConvertToInvariantString(model.LastLogin)), + new QueryParameter(nameof(User.NameSpan), model.NameSpan.ToString()), + new QueryParameter(nameof(User.Score), ConvertToInvariantString(model.Score)), + new QueryParameter(nameof(User.Age), ConvertToInvariantString(model.Age)), + new QueryParameter(nameof(User.Special), ConvertToInvariantString(model.Special)) + } + ); + + request.Parameters.Should().BeEquivalentTo(expectedParameters); + } + + sealed record User(Guid Id, string FirstName, string? LastName, DateTime? LastLogin, int Score, uint Age, nint Special) { + public ReadOnlySpan NameSpan => FirstName.AsSpan(); + } +}