diff --git a/src/RestSharp/Extensions/MiscExtensions.cs b/src/RestSharp/Extensions/MiscExtensions.cs index a6e94ff22..e399adc2d 100644 --- a/src/RestSharp/Extensions/MiscExtensions.cs +++ b/src/RestSharp/Extensions/MiscExtensions.cs @@ -39,4 +39,39 @@ public static async Task ReadAsBytes(this Stream input, CancellationToke return ms.ToArray(); } + + internal static IEnumerable<(string Name, object Value)> GetProperties(this object obj, params string[] includedProperties) { + // automatically create parameters from object props + var type = obj.GetType(); + var props = type.GetProperties(); + + foreach (var prop in props) { + if (!IsAllowedProperty(prop.Name)) + continue; + + var val = prop.GetValue(obj, null); + + if (val == null) + continue; + + var propType = prop.PropertyType; + + if (propType.IsArray) { + var elementType = propType.GetElementType(); + var array = (Array)val; + + if (array.Length > 0 && elementType != null) { + // convert the array to an array of strings + var values = array.Cast().Select(item => item.ToString()); + + val = string.Join(",", values); + } + } + + yield return(prop.Name, val); + } + + bool IsAllowedProperty(string propertyName) + => includedProperties.Length == 0 || includedProperties.Length > 0 && includedProperties.Contains(propertyName); + } } \ No newline at end of file diff --git a/src/RestSharp/Parameters/BodyParameter.cs b/src/RestSharp/Parameters/BodyParameter.cs index 4052a1049..8573c09e2 100644 --- a/src/RestSharp/Parameters/BodyParameter.cs +++ b/src/RestSharp/Parameters/BodyParameter.cs @@ -17,7 +17,7 @@ namespace RestSharp; public record BodyParameter : Parameter { public BodyParameter(string? name, object value, string contentType, DataFormat dataFormat = DataFormat.None) - : base(name, Ensure.NotNull(value, nameof(value)), ParameterType.RequestBody) { + : base(name, Ensure.NotNull(value, nameof(value)), ParameterType.RequestBody, false) { ContentType = contentType; DataFormat = dataFormat; } diff --git a/src/RestSharp/Parameters/FileParameter.cs b/src/RestSharp/Parameters/FileParameter.cs index 2a337d0ec..d362f2fd6 100644 --- a/src/RestSharp/Parameters/FileParameter.cs +++ b/src/RestSharp/Parameters/FileParameter.cs @@ -77,7 +77,7 @@ Stream GetFile() { /// Delegate that will be called with the request stream so you can write to it.. /// The length of the data that will be written by te writer. /// The filename to use in the request. - /// Optional: parameter content type + /// Optional: parameter content type, default is "application/g-zip" /// The using the default content type. public static FileParameter Create( string name, @@ -86,7 +86,7 @@ public static FileParameter Create( string fileName, string? contentType = null ) - => new(name, fileName, contentLength, getFile, contentType); + => new(name, fileName, contentLength, getFile, contentType ?? Serializers.ContentType.File); public static FileParameter FromFile(string fullPath, string? name = null, string? contentType = null) { if (!File.Exists(Ensure.NotEmptyString(fullPath, nameof(fullPath)))) diff --git a/src/RestSharp/Parameters/HeaderParameter.cs b/src/RestSharp/Parameters/HeaderParameter.cs index 286f8ef23..b2bf4ecc5 100644 --- a/src/RestSharp/Parameters/HeaderParameter.cs +++ b/src/RestSharp/Parameters/HeaderParameter.cs @@ -16,5 +16,5 @@ namespace RestSharp; public record HeaderParameter : Parameter { - public HeaderParameter(string? name, object? value, bool encode = false) : base(name, value, ParameterType.HttpHeader, encode) { } + public HeaderParameter(string? name, object? value) : base(name, value, ParameterType.HttpHeader, false) { } } \ No newline at end of file diff --git a/src/RestSharp/Parameters/Parameter.cs b/src/RestSharp/Parameters/Parameter.cs index 5ead0fbc4..f87bbb9b8 100644 --- a/src/RestSharp/Parameters/Parameter.cs +++ b/src/RestSharp/Parameters/Parameter.cs @@ -17,34 +17,7 @@ namespace RestSharp; /// /// Parameter container for REST requests /// -public record Parameter { - protected Parameter(string? name, object? value, ParameterType type, bool encode = true) { - Name = name; - Value = value; - Type = type; - Encode = encode; - } - - // Parameter(string name, object value, string contentType, ParameterType type, bool encode = true) : this(name, value, type, encode) - // => ContentType = contentType; - - /// - /// Name of the parameter - /// - public string? Name { get; } - - /// - /// Value of the parameter - /// - public object? Value { get; } - - /// - /// Type of the parameter - /// - public ParameterType Type { get; } - - internal bool Encode { get; } - +public abstract record Parameter(string? Name, object? Value, ParameterType Type, bool Encode) { /// /// MIME content type of the parameter /// @@ -60,7 +33,7 @@ public static Parameter CreateParameter(string? name, object value, ParameterTyp => type switch { ParameterType.GetOrPost => new GetOrPostParameter(name!, value, encode), ParameterType.UrlSegment => new UrlSegmentParameter(name!, value, encode), - ParameterType.HttpHeader => new HeaderParameter(name, value, encode), + ParameterType.HttpHeader => new HeaderParameter(name, value), ParameterType.RequestBody => new BodyParameter(name, value, Serializers.ContentType.Plain), ParameterType.QueryString => new QueryParameter(name!, value, encode), _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) diff --git a/src/RestSharp/Parameters/ParametersCollection.cs b/src/RestSharp/Parameters/ParametersCollection.cs index 3747810fc..7ba4dc668 100644 --- a/src/RestSharp/Parameters/ParametersCollection.cs +++ b/src/RestSharp/Parameters/ParametersCollection.cs @@ -50,18 +50,21 @@ public bool Exists(Parameter parameter) internal ParametersCollection GetParameters(ParameterType parameterType) => new(_parameters.Where(x => x.Type == parameterType)); - internal ParametersCollection GetQueryParameters(Method method) - => new( - method is not Method.Post and not Method.Put and not Method.Patch - ? _parameters - .Where( - p => p.Type is ParameterType.GetOrPost or ParameterType.QueryString - ) - : _parameters - .Where( - p => p.Type is ParameterType.QueryString - ) - ); + internal ParametersCollection GetParameters() => new(_parameters.Where(x => x is T)); + + internal ParametersCollection GetQueryParameters(Method method) { + Func condition = + !IsPostStyle(method) + ? p => p.Type is ParameterType.GetOrPost or ParameterType.QueryString + : p => p.Type is ParameterType.QueryString; + + return new ParametersCollection(_parameters.Where(p => condition(p))); + } + + internal ParametersCollection? GetContentParameters(Method method) + => !IsPostStyle(method) ? null : new ParametersCollection(GetParameters()); + + static bool IsPostStyle(Method method) => method is Method.Post or Method.Put or Method.Patch; public IEnumerator GetEnumerator() => _parameters.GetEnumerator(); diff --git a/src/RestSharp/Parameters/UrlSegmentParameter.cs b/src/RestSharp/Parameters/UrlSegmentParameter.cs index 80b550e8e..f643953cf 100644 --- a/src/RestSharp/Parameters/UrlSegmentParameter.cs +++ b/src/RestSharp/Parameters/UrlSegmentParameter.cs @@ -13,9 +13,9 @@ // limitations under the License. // -namespace RestSharp; +namespace RestSharp; public record UrlSegmentParameter : NamedParameter { - public UrlSegmentParameter(string name, object? value, bool encode = true) - : base(name, value?.ToString()?.Replace("%2F", "/").Replace("%2f", "/"), ParameterType.UrlSegment, encode) { } + public UrlSegmentParameter(string name, object value, bool encode = true) + : base(name, Ensure.NotEmptyString(value, nameof(value)).Replace("%2F", "/").Replace("%2f", "/"), ParameterType.UrlSegment, encode) { } } \ No newline at end of file diff --git a/src/RestSharp/Request/BodyExtensions.cs b/src/RestSharp/Request/BodyExtensions.cs index 3f849d197..4ee29d84e 100644 --- a/src/RestSharp/Request/BodyExtensions.cs +++ b/src/RestSharp/Request/BodyExtensions.cs @@ -21,10 +21,7 @@ public static bool TryGetBodyParameter(this RestRequest request, out BodyParamet return bodyParameter != null; } - public static Parameter[] GetPostParameters(this RestRequest request) - => request.Parameters.Where(x => x.Type == ParameterType.GetOrPost).ToArray(); - - public static bool HasPostParameters(this RestRequest request) => request.Parameters.Any(x => x.Type == ParameterType.GetOrPost); - public static bool HasFiles(this RestRequest request) => request.Files.Count > 0; + + public static bool IsEmpty(this ParametersCollection? parameters) => parameters == null || parameters.Count == 0; } \ No newline at end of file diff --git a/src/RestSharp/Request/HttpRequestMessageExtensions.cs b/src/RestSharp/Request/HttpRequestMessageExtensions.cs index ea50a6c35..0276fceaf 100644 --- a/src/RestSharp/Request/HttpRequestMessageExtensions.cs +++ b/src/RestSharp/Request/HttpRequestMessageExtensions.cs @@ -18,18 +18,14 @@ namespace RestSharp; static class HttpRequestMessageExtensions { - public static void AddHeaders(this HttpRequestMessage message, ParametersCollection parameters, Func encode) { - var headerParameters = parameters - .GetParameters(ParameterType.HttpHeader) - .Where(x => !RequestContent.ContentHeaders.Contains(x.Name)); + public static void AddHeaders(this HttpRequestMessage message, RequestHeaders headers) { + var headerParameters = headers.Parameters.Where(x => !RequestContent.ContentHeaders.Contains(x.Name)); headerParameters.ForEach(AddHeader); void AddHeader(Parameter parameter) { var parameterStringValue = parameter.Value!.ToString(); - if (parameter.Encode) parameterStringValue = encode(parameterStringValue!); - message.Headers.Remove(parameter.Name!); message.Headers.TryAddWithoutValidation(parameter.Name!, parameterStringValue); } diff --git a/src/RestSharp/Request/RequestContent.cs b/src/RestSharp/Request/RequestContent.cs index 1a2ab3b90..e969df0bc 100644 --- a/src/RestSharp/Request/RequestContent.cs +++ b/src/RestSharp/Request/RequestContent.cs @@ -39,8 +39,9 @@ public RequestContent(RestClient client, RestRequest request) { public HttpContent BuildContent() { AddFiles(); - AddBody(); - AddPostParameters(); + var postParameters = _request.Parameters.GetContentParameters(_request.Method); + AddBody(!postParameters.IsEmpty()); + AddPostParameters(postParameters); AddHeaders(); return Content!; } @@ -95,13 +96,13 @@ static bool BodyShouldBeMultipartForm(BodyParameter bodyParameter) { return bodyParameter.Name.IsNotEmpty() && bodyParameter.Name != bodyContentType; } - void AddBody() { + void AddBody(bool hasPostParameters) { if (!_request.TryGetBodyParameter(out var bodyParameter)) return; var bodyContent = Serialize(bodyParameter!); // we need to send the body - if (_request.HasPostParameters() || _request.HasFiles() || BodyShouldBeMultipartForm(bodyParameter!)) { + if (hasPostParameters || _request.HasFiles() || BodyShouldBeMultipartForm(bodyParameter!)) { // here we must use multipart form data var mpContent = Content as MultipartFormDataContent ?? new MultipartFormDataContent(); @@ -117,29 +118,20 @@ void AddBody() { } } - void AddPostParameters() { - var postParameters = _request.GetPostParameters(); - if (postParameters.Length <= 0) return; - - // it's a form - if (Content is MultipartFormDataContent mpContent) { - // we got the multipart form already instantiated, just add parameters to it - foreach (var postParameter in postParameters) { - mpContent.Add( - new StringContent(postParameter.Value!.ToString()!, _client.Options.Encoding, postParameter.ContentType), - postParameter.Name! - ); - } - } - else { - // we should not have anything else except the parameters, so we send them as form URL encoded - var formContent = new FormUrlEncodedContent( - _request.Parameters - .Where(x => x.Type == ParameterType.GetOrPost) - .Select(x => new KeyValuePair(x.Name!, x.Value!.ToString()!))! + void AddPostParameters(ParametersCollection? postParameters) { + if (postParameters.IsEmpty()) return; + + var mpContent = Content as MultipartFormDataContent ?? new MultipartFormDataContent(); + + // we got the multipart form already instantiated, just add parameters to it + foreach (var postParameter in postParameters!) { + mpContent.Add( + new StringContent(postParameter.Value!.ToString()!, _client.Options.Encoding, postParameter.ContentType), + postParameter.Name! ); - Content = formContent; } + + Content = mpContent; } void AddHeaders() { diff --git a/src/RestSharp/Request/RequestParameters.cs b/src/RestSharp/Request/RequestHeaders.cs similarity index 55% rename from src/RestSharp/Request/RequestParameters.cs rename to src/RestSharp/Request/RequestHeaders.cs index 6d72c3cfa..c5a89e09c 100644 --- a/src/RestSharp/Request/RequestParameters.cs +++ b/src/RestSharp/Request/RequestHeaders.cs @@ -15,31 +15,16 @@ namespace RestSharp; -class RequestParameters { - static readonly ParameterType[] MultiParameterTypes = { ParameterType.QueryString, ParameterType.GetOrPost }; - +class RequestHeaders { public ParametersCollection Parameters { get; } = new(); - public RequestParameters AddParameters(ParametersCollection parameters, bool allowSameName) { - Parameters.AddParameters(GetParameters(parameters, allowSameName)); + public RequestHeaders AddHeaders(ParametersCollection parameters) { + Parameters.AddParameters(parameters.GetParameters()); return this; } - IEnumerable GetParameters(ParametersCollection parametersCollection, bool allowSameName) { - foreach (var parameter in parametersCollection) { - var parameterExists = Parameters.Exists(parameter); - - if (allowSameName) { - var isMultiParameter = MultiParameterTypes.Any(pt => pt == parameter.Type); - parameterExists = !isMultiParameter && parameterExists; - } - - if (!parameterExists) yield return parameter; - } - } - // Add Accept header based on registered deserializers if none has been set by the caller. - public RequestParameters AddAcceptHeader(string[] acceptedContentTypes) { + public RequestHeaders AddAcceptHeader(string[] acceptedContentTypes) { if (Parameters.TryFind(KnownHeaders.Accept) == null) { var accepts = string.Join(", ", acceptedContentTypes); Parameters.AddParameter(new HeaderParameter(KnownHeaders.Accept, accepts)); diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs index 25a3c1256..b1aaafe54 100644 --- a/src/RestSharp/Request/RestRequest.cs +++ b/src/RestSharp/Request/RestRequest.cs @@ -13,6 +13,7 @@ // limitations under the License. using RestSharp.Extensions; +// ReSharper disable UnusedAutoPropertyAccessor.Global namespace RestSharp; @@ -26,15 +27,11 @@ public class RestRequest { /// /// Default constructor /// - public RestRequest() { - RequestFormat = DataFormat.Json; - Method = Method.Get; - } + public RestRequest() => Method = Method.Get; - public RestRequest(string? resource, Method method = Method.Get, DataFormat dataFormat = DataFormat.Json) : this() { - Resource = resource ?? ""; - Method = method; - RequestFormat = dataFormat; + public RestRequest(string? resource, Method method = Method.Get) : this() { + Resource = resource ?? ""; + Method = method; if (string.IsNullOrWhiteSpace(resource)) return; @@ -61,17 +58,11 @@ static IEnumerable> ParseQuery(string query) ); } - public RestRequest(Uri resource, Method method = Method.Get, DataFormat dataFormat = DataFormat.Json) - : this( - resource.IsAbsoluteUri - ? resource.AbsoluteUri - : resource.OriginalString, - method, - dataFormat - ) { } + public RestRequest(Uri resource, Method method = Method.Get) + : this(resource.IsAbsoluteUri ? resource.AbsoluteUri : resource.OriginalString, method) { } // readonly List _parameters = new(); - readonly List _files = new(); + readonly List _files = new(); /// /// Always send a multipart/form-data request - even when no Files are present. diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index b43726549..4ce21fd29 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -58,14 +58,14 @@ public static RestRequest AddOrUpdateParameter(this RestRequest request, string public static RestRequest AddOrUpdateParameter(this RestRequest request, string name, object value, ParameterType type, bool encode = true) => request.AddOrUpdateParameter(Parameter.CreateParameter(name, value, type, encode)); - public static RestRequest AddHeader(this RestRequest request, string name, string value, bool encode = false) { + public static RestRequest AddHeader(this RestRequest request, string name, string value) { CheckAndThrowsForInvalidHost(name, value); - return request.AddParameter(new HeaderParameter(name, value, encode)); + return request.AddParameter(new HeaderParameter(name, value)); } - public static RestRequest AddOrUpdateHeader(this RestRequest request, string name, string value, bool encode = false) { + public static RestRequest AddOrUpdateHeader(this RestRequest request, string name, string value) { CheckAndThrowsForInvalidHost(name, value); - return request.AddOrUpdateParameter(name, value, ParameterType.HttpHeader, encode); + return request.AddOrUpdateParameter(new HeaderParameter(name, value)); } public static RestRequest AddHeaders(this RestRequest request, ICollection> headers) { @@ -97,11 +97,28 @@ public static RestRequest AddQueryParameter(this RestRequest request, string nam public static RestRequest AddUrlSegment(this RestRequest request, string name, object value, bool encode = true) => request.AddParameter(new UrlSegmentParameter(name, value, encode)); + /// + /// Adds a file parameter to the request body. The file will be read from disk as a stream. + /// + /// Request instance + /// Parameter name + /// Full path to the file + /// Optional: content type + /// public static RestRequest AddFile(this RestRequest request, string name, string path, string? contentType = null) => request.AddFile(FileParameter.FromFile(path, name, contentType)); - public static RestRequest AddFile(this RestRequest request, string name, byte[] bytes, string fileName, string? contentType = null) - => request.AddFile(FileParameter.Create(name, bytes, fileName, contentType)); + /// + /// Adds bytes to the request as file attachment + /// + /// Request instance + /// Parameter name + /// File content as bytes + /// File name + /// Optional: content type. Default is "application/octet-stream" + /// + public static RestRequest AddFile(this RestRequest request, string name, byte[] bytes, string filename, string? contentType = null) + => request.AddFile(FileParameter.Create(name, bytes, filename, contentType)); public static RestRequest AddFile( this RestRequest request, @@ -113,86 +130,72 @@ public static RestRequest AddFile( ) => request.AddFile(FileParameter.Create(name, getFile, contentLength, fileName, contentType)); - public static RestRequest AddFileBytes( - this RestRequest request, - string name, - byte[] bytes, - string filename, - string contentType = "application/x-gzip" - ) - => request.AddFile(FileParameter.Create(name, bytes, filename, contentType)); + /// + /// Adds a body parameter to the request + /// + /// Request instance + /// Object to be used as the request body, or string for plain content + /// Optional: content type + /// + /// Thrown if request body type cannot be resolved + /// This method will try to figure out the right content type based on the request data format and the provided content type + public static RestRequest AddBody(this RestRequest request, object obj, string? contentType = null) { + if (contentType == null) { + return request.RequestFormat switch { + DataFormat.Json => request.AddJsonBody(obj, contentType ?? ContentType.Json), + DataFormat.Xml => request.AddXmlBody(obj, contentType ?? ContentType.Xml), + _ => request.AddParameter(new BodyParameter("", obj.ToString()!, contentType ?? ContentType.Plain)) + }; + } - public static RestRequest AddBody(this RestRequest request, object obj, string xmlNamespace) - => request.RequestFormat switch { - DataFormat.Json => request.AddJsonBody(obj), - DataFormat.Xml => request.AddXmlBody(obj, xmlNamespace), - _ => request - }; - - public static RestRequest AddBody(this RestRequest request, object obj) - => request.RequestFormat switch { - DataFormat.Json => request.AddJsonBody(obj), - DataFormat.Xml => request.AddXmlBody(obj), - _ => request.AddParameter(new BodyParameter("", obj.ToString()!, ContentType.Plain)) - }; - - public static RestRequest AddJsonBody(this RestRequest request, object obj) { - request.RequestFormat = DataFormat.Json; - return request.AddParameter(new JsonParameter("", obj)); + return + obj is string str ? request.AddParameter(new BodyParameter("", str, contentType)) : + contentType.Contains("xml") ? request.AddXmlBody(obj, contentType) : + contentType.Contains("json") ? request.AddJsonBody(obj, contentType) : + throw new ArgumentException("Non-string body found with unsupported content type", nameof(obj)); } - public static RestRequest AddJsonBody(this RestRequest request, object obj, string contentType) { + /// + /// Adds a JSON body parameter to the request + /// + /// Request instance + /// Object that will be serialized to JSON + /// Optional: content type. Default is "application/json" + /// + public static RestRequest AddJsonBody(this RestRequest request, object obj, string contentType = ContentType.Json) { request.RequestFormat = DataFormat.Json; - return request.AddParameter(new JsonParameter(contentType, obj, contentType)); + return request.AddParameter(new JsonParameter("", obj, contentType)); } - public static RestRequest AddXmlBody(this RestRequest request, object obj) => request.AddXmlBody(obj, ""); - - public static RestRequest AddXmlBody(this RestRequest request, object obj, string xmlNamespace) { + /// + /// Adds an XML body parameter to the request + /// + /// Request instance + /// Object that will be serialized to XML + /// Optional: content type. Default is "application/xml" + /// Optional: XML namespace + /// + public static RestRequest AddXmlBody(this RestRequest request, object obj, string contentType = ContentType.Xml, string xmlNamespace = "") { request.RequestFormat = DataFormat.Xml; - request.AddParameter(new XmlParameter("", obj, xmlNamespace)); + request.AddParameter(new XmlParameter("", obj, xmlNamespace, contentType)); return request; } + /// + /// Gets object properties and adds each property as a form data parameter + /// + /// Request instance + /// Object to add as form data + /// Properties to include, or nothing to include everything + /// public static RestRequest AddObject(this RestRequest request, object obj, params string[] includedProperties) { - // automatically create parameters from object props - var type = obj.GetType(); - var props = type.GetProperties(); - - foreach (var prop in props) { - if (!IsAllowedProperty(prop.Name)) - continue; - - var val = prop.GetValue(obj, null); - - if (val == null) - continue; + var props = obj.GetProperties(includedProperties); - var propType = prop.PropertyType; - - if (propType.IsArray) { - var elementType = propType.GetElementType(); - var array = (Array)val; - - if (array.Length > 0 && elementType != null) { - // convert the array to an array of strings - var values = array.Cast().Select(item => item.ToString()); - - val = string.Join(",", values); - } - } - - request.AddParameter(prop.Name, val); + foreach (var (name, value) in props) { + request.AddParameter(name, value); } return request; - - bool IsAllowedProperty(string propertyName) - => includedProperties.Length == 0 || includedProperties.Length > 0 && includedProperties.Contains(propertyName); - } - - public static RestRequest AddObject(this RestRequest request, object obj) { - return request.With(x => x.AddObject(obj, new string[] { })); } static void CheckAndThrowsForInvalidHost(string name, string value) { diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index 32891ec94..49ac4c773 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -31,7 +31,7 @@ public async Task ExecuteAsync(RestRequest request, CancellationTo ? await RestResponse.FromHttpResponse( internalResponse.ResponseMessage!, request, - _cookieContainer.GetCookies(internalResponse.Url), + CookieContainer.GetCookies(internalResponse.Url), cancellationToken ) : ReturnErrorOrThrow(response, internalResponse.Exception, internalResponse.TimeoutToken); @@ -60,11 +60,11 @@ async Task ExecuteInternal(RestRequest request, CancellationTo var ct = cts.Token; try { - var parameters = new RequestParameters() - .AddParameters(request.Parameters, true) - .AddParameters(DefaultParameters, Options.AllowMultipleDefaultParametersWithSameName) + var headers = new RequestHeaders() + .AddHeaders(request.Parameters) + .AddHeaders(DefaultParameters) .AddAcceptHeader(AcceptedContentTypes); - message.AddHeaders(parameters.Parameters, Encode); + message.AddHeaders(headers); if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message); diff --git a/src/RestSharp/RestClient.Serialization.cs b/src/RestSharp/RestClient.Serialization.cs new file mode 100644 index 000000000..0f3441ca5 --- /dev/null +++ b/src/RestSharp/RestClient.Serialization.cs @@ -0,0 +1,109 @@ +// Copyright © 2009-2021 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. + +using RestSharp.Extensions; +using RestSharp.Serializers; +using RestSharp.Serializers.Json; +using RestSharp.Serializers.Xml; + +// ReSharper disable VirtualMemberCallInConstructor +#pragma warning disable 618 + +namespace RestSharp; + +public partial class RestClient { + internal Dictionary Serializers { get; } = new(); + + [PublicAPI] + public RestResponse Deserialize(RestResponse response) => Deserialize(response.Request!, response); + + /// + /// Replace the default serializer with a custom one + /// + /// Function that returns the serializer instance + public RestClient UseSerializer(Func serializerFactory) { + var instance = serializerFactory(); + Serializers[instance.DataFormat] = new SerializerRecord(instance.DataFormat, instance.SupportedContentTypes, serializerFactory); + AssignAcceptedContentTypes(); + return this; + } + + public void UseDefaultSerializers() { + UseSerializer(); + UseSerializer(); + } + + /// + /// Replace the default serializer with a custom one + /// + /// The type that implements + /// + public RestClient UseSerializer() where T : class, IRestSerializer, new() => UseSerializer(() => new T()); + + internal RestResponse Deserialize(RestRequest request, RestResponse raw) { + var response = RestResponse.FromResponse(raw); + + try { + request.OnBeforeDeserialization?.Invoke(raw); + + // Only attempt to deserialize if the request has not errored due + // to a transport or framework exception. HTTP errors should attempt to + // be deserialized + if (response.ErrorException == null) { + var handler = GetContentDeserializer(raw, request.RequestFormat); + + // Only continue if there is a handler defined else there is no way to deserialize the data. + // This can happen when a request returns for example a 404 page instead of the requested JSON/XML resource + if (handler is IXmlDeserializer xml && request is RestXmlRequest xmlRequest) { + if (xmlRequest.XmlNamespace.IsNotEmpty()) xml.Namespace = xmlRequest.XmlNamespace!; + + if (xml is IWithDateFormat withDateFormat && xmlRequest.DateFormat.IsNotEmpty()) + withDateFormat.DateFormat = xmlRequest.DateFormat!; + } + + if (handler is IWithRootElement deserializer && !request.RootElement.IsEmpty()) deserializer.RootElement = request.RootElement; + + if (handler != null) response.Data = handler.Deserialize(raw); + } + } + catch (Exception ex) { + if (Options.ThrowOnAnyError) throw; + + if (Options.FailOnDeserializationError || Options.ThrowOnDeserializationError) response.ResponseStatus = ResponseStatus.Error; + + response.ErrorMessage = ex.Message; + response.ErrorException = ex; + + if (Options.ThrowOnDeserializationError) throw new DeserializationException(response, ex); + } + + response.Request = request; + + return response; + } + + IDeserializer? GetContentDeserializer(RestResponseBase response, DataFormat requestFormat) { + var contentType = response.ContentType != null && AcceptedContentTypes.Contains(response.ContentType) + ? response.ContentType + : DetectContentType(); + if (contentType.IsEmpty()) return null; + + var serializer = Serializers.FirstOrDefault(x => x.Value.SupportedContentTypes.Contains(contentType)); + var factory = serializer.Value ?? (Serializers.ContainsKey(requestFormat) ? Serializers[requestFormat] : null); + return factory?.GetSerializer().Deserializer; + + string? DetectContentType() + => response.Content!.StartsWith("<") ? ContentType.Xml : response.Content.StartsWith("{") ? ContentType.Json : null; + } +} \ No newline at end of file diff --git a/src/RestSharp/RestClient.cs b/src/RestSharp/RestClient.cs index 74280e702..263ae5cd9 100644 --- a/src/RestSharp/RestClient.cs +++ b/src/RestSharp/RestClient.cs @@ -16,7 +16,6 @@ using System.Text; using RestSharp.Authenticators; using RestSharp.Extensions; -using RestSharp.Serializers; using RestSharp.Serializers.Json; using RestSharp.Serializers.Xml; @@ -29,8 +28,9 @@ namespace RestSharp; /// Client to translate RestRequests into Http requests and process response result /// public partial class RestClient { - readonly CookieContainer _cookieContainer; - // readonly List _defaultParameters = new(); + public CookieContainer CookieContainer { get; } + + public string[] AcceptedContentTypes { get; private set; } = null!; HttpClient HttpClient { get; } @@ -42,12 +42,11 @@ public partial class RestClient { public RestClient() : this(new RestClientOptions()) { } public RestClient(HttpClient httpClient, RestClientOptions? options = null) { - UseSerializer(); - UseSerializer(); + UseDefaultSerializers(); - HttpClient = httpClient; - Options = options ?? new RestClientOptions(); - _cookieContainer = Options.CookieContainer ?? new CookieContainer(); + HttpClient = httpClient; + Options = options ?? new RestClientOptions(); + CookieContainer = Options.CookieContainer ?? new CookieContainer(); if (Options.Timeout > 0) HttpClient.Timeout = TimeSpan.FromMilliseconds(Options.Timeout); @@ -57,16 +56,15 @@ public RestClient(HttpClient httpClient, RestClientOptions? options = null) { public RestClient(HttpMessageHandler handler) : this(new HttpClient(handler)) { } public RestClient(RestClientOptions options) { - UseSerializer(); - UseSerializer(); + UseDefaultSerializers(); - Options = options; - _cookieContainer = Options.CookieContainer ?? new CookieContainer(); + Options = options; + CookieContainer = Options.CookieContainer ?? new CookieContainer(); var handler = new HttpClientHandler { Credentials = Options.Credentials, UseDefaultCredentials = Options.UseDefaultCredentials, - CookieContainer = _cookieContainer, + CookieContainer = CookieContainer, AutomaticDecompression = Options.AutomaticDecompression, PreAuthenticate = Options.PreAuthenticate, AllowAutoRedirect = Options.FollowRedirects, @@ -106,8 +104,6 @@ public RestClient(Uri baseUrl) : this(new RestClientOptions { BaseUrl = baseUrl /// public RestClient(string baseUrl) : this(new Uri(Ensure.NotEmptyString(baseUrl, nameof(baseUrl)))) { } - internal Dictionary Serializers { get; } = new(); - Func Encode { get; set; } = s => s.UrlEncode(); Func EncodeQuery { get; set; } = (s, encoding) => s.UrlEncode(encoding)!; @@ -133,14 +129,17 @@ public RestClient(string baseUrl) : this(new Uri(Ensure.NotEmptyString(baseUrl, /// public IAuthenticator? Authenticator { get; set; } - // public IReadOnlyCollection DefaultParameters { - // get { lock (_defaultParameters) return _defaultParameters; } - // } public ParametersCollection DefaultParameters { get; } = new(); + /// + /// Adds cookie to the cookie container. + /// + /// Cookie name + /// Cookie value + /// public RestClient AddCookie(string name, string value) { - lock (_cookieContainer) { - _cookieContainer.Add(new Cookie(name, value)); + lock (CookieContainer) { + CookieContainer.Add(new Cookie(name, value)); } return this; @@ -149,21 +148,26 @@ public RestClient AddCookie(string name, string value) { /// /// Add a parameter to use on every request made with this client instance /// - /// Parameter to add + /// Parameter to add /// - public RestClient AddDefaultParameter(Parameter p) { - if (p.Type == ParameterType.RequestBody) + public RestClient AddDefaultParameter(Parameter parameter) { + if (parameter.Type == ParameterType.RequestBody) throw new NotSupportedException( "Cannot set request body using default parameters. Use Request.AddBody() instead." ); - DefaultParameters.AddParameter(p); + if (!Options.AllowMultipleDefaultParametersWithSameName && + !MultiParameterTypes.Contains(parameter.Type) && + DefaultParameters.Any(x => x.Name == parameter.Name)) { + throw new ArgumentException("A default parameters with the same name has already been added", nameof(parameter)); + } + + DefaultParameters.AddParameter(parameter); return this; } - [PublicAPI] - public RestResponse Deserialize(RestResponse response) => Deserialize(response.Request!, response); + static readonly ParameterType[] MultiParameterTypes = { ParameterType.QueryString, ParameterType.GetOrPost }; public Uri BuildUri(RestRequest request) { DoBuildUriValidations(request); @@ -187,105 +191,14 @@ internal Uri BuildUriWithoutQueryParameters(RestRequest request) { return uri.MergeBaseUrlAndResource(resource); } - public string[] AcceptedContentTypes { get; private set; } = null!; - internal void AssignAcceptedContentTypes() => AcceptedContentTypes = Serializers.SelectMany(x => x.Value.SupportedContentTypes).Distinct().ToArray(); - /// - /// Replace the default serializer with a custom one - /// - /// Function that returns the serializer instance - public RestClient UseSerializer(Func serializerFactory) { - var instance = serializerFactory(); - Serializers[instance.DataFormat] = new SerializerRecord(instance.DataFormat, instance.SupportedContentTypes, serializerFactory); - AssignAcceptedContentTypes(); - return this; - } - - /// - /// Replace the default serializer with a custom one - /// - /// The type that implements - /// - public RestClient UseSerializer() where T : class, IRestSerializer, new() => UseSerializer(() => new T()); - void DoBuildUriValidations(RestRequest request) { if (Options.BaseUrl == null && !request.Resource.ToLowerInvariant().StartsWith("http")) throw new ArgumentOutOfRangeException( nameof(request), "Request resource doesn't contain a valid scheme for an empty client base URL" ); - - var nullValuedParams = request.Parameters - .GetParameters(ParameterType.UrlSegment) - .Where(p => p.Value == null) - .Select(p => p.Name) - .ToArray(); - - if (nullValuedParams.Any()) { - var names = nullValuedParams.JoinToString(", ", name => $"'{name}'"); - - throw new ArgumentException( - $"Cannot build uri when url segment parameter(s) {names} value is null.", - nameof(request) - ); - } - } - - internal RestResponse Deserialize(RestRequest request, RestResponse raw) { - var response = RestResponse.FromResponse(raw); - - try { - request.OnBeforeDeserialization?.Invoke(raw); - - // Only attempt to deserialize if the request has not errored due - // to a transport or framework exception. HTTP errors should attempt to - // be deserialized - if (response.ErrorException == null) { - var handler = GetContentDeserializer(raw, request.RequestFormat); - - // Only continue if there is a handler defined else there is no way to deserialize the data. - // This can happen when a request returns for example a 404 page instead of the requested JSON/XML resource - if (handler is IXmlDeserializer xml && request is RestXmlRequest xmlRequest) { - if (xmlRequest.XmlNamespace.IsNotEmpty()) xml.Namespace = xmlRequest.XmlNamespace!; - - if (xml is IWithDateFormat withDateFormat && xmlRequest.DateFormat.IsNotEmpty()) - withDateFormat.DateFormat = xmlRequest.DateFormat!; - } - - if (handler is IWithRootElement deserializer && !request.RootElement.IsEmpty()) deserializer.RootElement = request.RootElement; - - if (handler != null) response.Data = handler.Deserialize(raw); - } - } - catch (Exception ex) { - if (Options.ThrowOnAnyError) throw; - - if (Options.FailOnDeserializationError || Options.ThrowOnDeserializationError) response.ResponseStatus = ResponseStatus.Error; - - response.ErrorMessage = ex.Message; - response.ErrorException = ex; - - if (Options.ThrowOnDeserializationError) throw new DeserializationException(response, ex); - } - - response.Request = request; - - return response; - } - - IDeserializer? GetContentDeserializer(RestResponseBase response, DataFormat requestFormat) { - var contentType = response.ContentType != null && AcceptedContentTypes.Contains(response.ContentType) - ? response.ContentType - : DetectContentType(); - if (contentType.IsEmpty()) return null; - - var serializer = Serializers.FirstOrDefault(x => x.Value.SupportedContentTypes.Contains(contentType)); - var factory = serializer.Value ?? (Serializers.ContainsKey(requestFormat) ? Serializers[requestFormat] : null); - return factory?.GetSerializer().Deserializer; - - string? DetectContentType() - => response.Content!.StartsWith("<") ? ContentType.Xml : response.Content.StartsWith("{") ? ContentType.Json : null; } } \ No newline at end of file diff --git a/src/RestSharp/RestClientExtensions.Json.cs b/src/RestSharp/RestClientExtensions.Json.cs index 6857f3a0b..115ca94f9 100644 --- a/src/RestSharp/RestClientExtensions.Json.cs +++ b/src/RestSharp/RestClientExtensions.Json.cs @@ -14,6 +14,7 @@ // using System.Net; +using RestSharp.Extensions; namespace RestSharp; @@ -31,6 +32,35 @@ public static partial class RestClientExtensions { return client.GetAsync(request, cancellationToken); } + public static Task GetJsonAsync( + this RestClient client, + string resource, + object parameters, + CancellationToken cancellationToken = default + ) { + var props = parameters.GetProperties(); + var query = new List(); + + foreach (var (name, value) in props) { + var param = $"{name}"; + + if (resource.Contains(param)) { + resource = resource.Replace(param, value.ToString()); + } + else { + query.Add(new QueryParameter(name, value)); + } + } + + var request = new RestRequest(resource); + + foreach (var parameter in query) { + request.AddParameter(parameter); + } + + return client.GetAsync(request, cancellationToken); + } + /// /// Serializes the request object to JSON and makes a POST call to the resource specified in the resource parameter. /// Expects a JSON response back, deserializes it to TResponse type and returns it. @@ -69,7 +99,7 @@ public static async Task PostJsonAsync( CancellationToken cancellationToken = default ) where TRequest : class { var restRequest = new RestRequest().AddJsonBody(request); - var response = await client.PostAsync(restRequest, cancellationToken); + var response = await client.PostAsync(restRequest, cancellationToken); return response.StatusCode; } @@ -93,7 +123,7 @@ public static async Task PostJsonAsync( var restRequest = new RestRequest().AddJsonBody(request); return client.PutAsync(restRequest, cancellationToken); } - + /// /// Serializes the request object to JSON and makes a PUT call to the resource specified in the resource parameter. /// Expects no response back, just the status code. @@ -111,7 +141,7 @@ public static async Task PutJsonAsync( CancellationToken cancellationToken = default ) where TRequest : class { var restRequest = new RestRequest().AddJsonBody(request); - var response = await client.PutAsync(restRequest, cancellationToken); + var response = await client.PutAsync(restRequest, cancellationToken); return response.StatusCode; } } \ No newline at end of file diff --git a/src/RestSharp/Serializers/ContentType.cs b/src/RestSharp/Serializers/ContentType.cs index d5f4ddf81..a8daca348 100644 --- a/src/RestSharp/Serializers/ContentType.cs +++ b/src/RestSharp/Serializers/ContentType.cs @@ -21,6 +21,10 @@ public static class ContentType { public const string Plain = "text/plain"; + public const string File = "application/octet-stream"; + + public const string GZip = "application/x-gzip"; + public static readonly Dictionary FromDataFormat = new() { { DataFormat.Xml, Xml }, diff --git a/test/RestSharp.Tests/ObjectParameterTests.cs b/test/RestSharp.Tests/ObjectParameterTests.cs index b2e578d10..2be042e6b 100644 --- a/test/RestSharp.Tests/ObjectParameterTests.cs +++ b/test/RestSharp.Tests/ObjectParameterTests.cs @@ -4,6 +4,8 @@ public class ObjectParameterTests { [Fact] public void Can_Add_Object_With_IntegerArray_property() { var request = new RestRequest(); - request.AddObject(new { 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 diff --git a/test/RestSharp.Tests/ParametersTests.cs b/test/RestSharp.Tests/ParametersTests.cs index 5b50fd662..fd1b7247b 100644 --- a/test/RestSharp.Tests/ParametersTests.cs +++ b/test/RestSharp.Tests/ParametersTests.cs @@ -13,8 +13,7 @@ public void AddDefaultHeadersUsingDictionary() { var expected = headers.Select(x => new HeaderParameter(x.Key, x.Value)); - var options = new RestClientOptions(BaseUrl); - var client = new RestClient(options); + var client = new RestClient(BaseUrl); client.AddDefaultHeaders(headers); expected.Should().BeSubsetOf(client.DefaultParameters); diff --git a/test/RestSharp.Tests/UrlBuilderTests.cs b/test/RestSharp.Tests/UrlBuilderTests.cs index aa3c79190..9421d115c 100644 --- a/test/RestSharp.Tests/UrlBuilderTests.cs +++ b/test/RestSharp.Tests/UrlBuilderTests.cs @@ -115,16 +115,8 @@ public void GET_with_multiple_instances_of_same_key() { [Fact] public void GET_with_resource_containing_null_token() { - var request = new RestRequest("/resource/{foo}", Method.Get); - - request.AddUrlSegment("foo", null); - - var client = new RestClient("http://example.com/api/1.0"); - var exception = Assert.Throws(() => client.BuildUri(request)); - - Assert.NotNull(exception); - Assert.False(string.IsNullOrEmpty(exception.Message)); - Assert.Contains("foo", exception.Message); + var request = new RestRequest("/resource/{foo}"); + Assert.Throws(() => request.AddUrlSegment("foo", null)); } [Fact]