diff --git a/docs/error-handling.md b/docs/error-handling.md index fdb9a16ea..a2400481b 100644 --- a/docs/error-handling.md +++ b/docs/error-handling.md @@ -36,9 +36,8 @@ Below you can find how different extensions deal with errors. Note that function | `ExecuteGetAsync` | No | | `ExecuteGetAsync` | No | | `ExecutePostAsync` | No | -| `ExecutePutAsync` | No | -| `ExecuteGetAsync` | No | | `ExecutePostAsync` | No | +| `ExecutePutAsync` | No | | `ExecutePutAsync` | No | | `GetAsync` | Yes | | `GetAsync` | Yes | diff --git a/docs/usage.md b/docs/usage.md index a6857a287..b1901f1ae 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -33,7 +33,7 @@ record TokenResponse { Next, we create the authenticator itself. It needs the API key and API key secret to call the token endpoint using basic HTTP authentication. In addition, we can extend the list of parameters with the base URL to convert it to a more generic OAuth2 authenticator. -The easiest way to create an authenticator is to inherit from the `AuthanticatorBase` base class: +The easiest way to create an authenticator is to inherit from the `AuthenticatorBase` base class: ```csharp public class TwitterAuthenticator : AuthenticatorBase { diff --git a/docs/v107/README.md b/docs/v107/README.md index a8d5e5859..87d7771b7 100644 --- a/docs/v107/README.md +++ b/docs/v107/README.md @@ -216,7 +216,7 @@ mockHttp.When("http://localhost/api/user/*") .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON // Instantiate the client normally, but replace the message handler -var client = new RestClient(...) { ConfigureMessageHandler = _ => mockHttp }; +var client = new RestClient(new RestClientOptions { ConfigureMessageHandler = _ => mockHttp }); var request = new RestRequest("http://localhost/api/user/1234"); var response = await client.GetAsync(request); diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c99c4fe4a..727121d98 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,7 +20,7 @@ - + diff --git a/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj b/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj index f93532064..dc5e238b4 100644 --- a/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj +++ b/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj @@ -1,6 +1,6 @@ - + diff --git a/src/RestSharp/Enum.cs b/src/RestSharp/Enum.cs index e046d4864..fd584bc8c 100644 --- a/src/RestSharp/Enum.cs +++ b/src/RestSharp/Enum.cs @@ -11,6 +11,7 @@ // 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.Net; namespace RestSharp; @@ -19,9 +20,44 @@ namespace RestSharp; /// public enum ParameterType { /// - /// Cookie parameter + /// A that will added to the QueryString for GET, DELETE, OPTIONS and HEAD requests; and form for POST and PUT requests. /// - GetOrPost, UrlSegment, HttpHeader, RequestBody, QueryString + /// + /// See . + /// + GetOrPost, + + /// + /// A that will be added to part of the url by replacing a {placeholder} within the absolute path. + /// + /// + /// See . + /// + UrlSegment, + + /// + /// A that will be added as a request header + /// + /// + /// See . + /// + HttpHeader, + + /// + /// A that will be added to the request body + /// + /// + /// See . + /// + RequestBody, + + /// + /// A that will be added to the query string + /// + /// + /// See . + /// + QueryString } /// @@ -55,4 +91,29 @@ public struct DateFormat { /// /// Status for responses (surprised?) /// -public enum ResponseStatus { None, Completed, Error, TimedOut, Aborted } \ No newline at end of file +public enum ResponseStatus { + /// + /// Not Applicable, for when the Request has not yet been made + /// + None, + + /// + /// for when the request is passes as a result of being true, or when the response is + /// + Completed, + + /// + /// for when the request fails due as a result of being false except for the case when the response is + /// + Error, + + /// + /// for when the Operation is cancelled due to the request taking longer than the length of time prescribed by or due to the timing out. + /// + TimedOut, + + /// + /// for when the Operation is cancelled, due to reasons other than + /// + Aborted +} \ No newline at end of file diff --git a/src/RestSharp/Parameters/ObjectParser.cs b/src/RestSharp/Parameters/ObjectParser.cs index 71e496798..fec2a0a85 100644 --- a/src/RestSharp/Parameters/ObjectParser.cs +++ b/src/RestSharp/Parameters/ObjectParser.cs @@ -18,12 +18,12 @@ namespace RestSharp; static class ObjectParser { - public static IEnumerable<(string Name, string? Value)> GetProperties(this object obj, params string[] includedProperties) { + public static IEnumerable GetProperties(this object obj, params string[] includedProperties) { // automatically create parameters from object props var type = obj.GetType(); var props = type.GetProperties(); - var properties = new List<(string Name, string? Value)>(); + var properties = new List(); foreach (var prop in props.Where(x => IsAllowedProperty(x.Name))) { var val = prop.GetValue(obj, null); @@ -38,14 +38,14 @@ static class ObjectParser { string? ParseValue(string? format, object? value) => format == null ? value?.ToString() : string.Format($"{{0:{format}}}", value); - IEnumerable<(string, string?)> GetArray(PropertyInfo propertyInfo, object? value) { + IEnumerable GetArray(PropertyInfo propertyInfo, object? value) { var elementType = propertyInfo.PropertyType.GetElementType(); var array = (Array)value!; var attribute = propertyInfo.GetCustomAttribute(); - var name = attribute?.Name ?? propertyInfo.Name; - + var name = attribute?.Name ?? propertyInfo.Name; var queryType = attribute?.ArrayQueryType ?? RequestArrayQueryType.CommaSeparated; + var encode = attribute?.Encode ?? true; if (array.Length > 0 && elementType != null) { // convert the array to an array of strings @@ -54,21 +54,20 @@ static class ObjectParser { .Select(item => ParseValue(attribute?.Format, item)); return queryType switch { - RequestArrayQueryType.CommaSeparated => new (string, string?)[] { (name, string.Join(",", values)) }, - RequestArrayQueryType.ArrayParameters => values.Select(x => ($"{name}[]", x)), + RequestArrayQueryType.CommaSeparated => new[] { new ParsedParameter(name, string.Join(",", values), encode) }, + RequestArrayQueryType.ArrayParameters => values.Select(x => new ParsedParameter($"{name}[]", x, encode)), _ => throw new ArgumentOutOfRangeException() }; - } - return new (string, string?)[] { (name, null) }; + return new ParsedParameter[] { new(name, null, encode) }; } - (string, string?) GetValue(PropertyInfo propertyInfo, object? value) { + ParsedParameter GetValue(PropertyInfo propertyInfo, object? value) { var attribute = propertyInfo.GetCustomAttribute(); var name = attribute?.Name ?? propertyInfo.Name; var val = ParseValue(attribute?.Format, value); - return (name, val); + return new ParsedParameter(name, val, attribute?.Encode ?? true); } bool IsAllowedProperty(string propertyName) @@ -78,11 +77,14 @@ bool IsAllowedProperty(string propertyName) } } +record ParsedParameter(string Name, string? Value, bool Encode); + [AttributeUsage(AttributeTargets.Property)] public class RequestPropertyAttribute : Attribute { public string? Name { get; set; } public string? Format { get; set; } public RequestArrayQueryType ArrayQueryType { get; set; } = RequestArrayQueryType.CommaSeparated; + public bool Encode { get; set; } = true; } public enum RequestArrayQueryType { CommaSeparated, ArrayParameters } diff --git a/src/RestSharp/Parameters/UrlSegmentParameter.cs b/src/RestSharp/Parameters/UrlSegmentParameter.cs index 04b60d65c..deb0b3bdc 100644 --- a/src/RestSharp/Parameters/UrlSegmentParameter.cs +++ b/src/RestSharp/Parameters/UrlSegmentParameter.cs @@ -16,7 +16,7 @@ namespace RestSharp; public record UrlSegmentParameter : NamedParameter { /// - /// Instantiates a new query parameter instance that will be added to the request URL part of the query string. + /// Instantiates a new query parameter instance that will be added to the request URL by replacing part of the absolute path. /// The request resource should have a placeholder {name} that will be replaced with the parameter value when the request is made. /// /// Parameter name diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index a35da5621..60466f8cd 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -277,10 +277,10 @@ public static RestRequest AddFile( this RestRequest request, string name, byte[] bytes, - string filename, + string fileName, string? contentType = null ) - => request.AddFile(FileParameter.Create(name, bytes, filename, contentType)); + => request.AddFile(FileParameter.Create(name, bytes, fileName, contentType)); public static RestRequest AddFile( this RestRequest request, @@ -315,7 +315,7 @@ public static RestRequest AddFile( /// Request instance /// Parameter name /// File content as bytes - /// File name + /// File name /// Optional: content type. Default is "application/octet-stream" /// File parameter header options /// @@ -323,11 +323,11 @@ public static RestRequest AddFile( this RestRequest request, string name, byte[] bytes, - string filename, + string fileName, string? contentType, FileParameterOptions? options ) - => request.AddFile(FileParameter.Create(name, bytes, filename, contentType, options)); + => 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 @@ -439,8 +439,8 @@ public static RestRequest AddXmlBody(this RestRequest request, T obj, string public static RestRequest AddObject(this RestRequest request, T obj, params string[] includedProperties) where T : class { var props = obj.GetProperties(includedProperties); - foreach (var (name, value) in props) { - request.AddParameter(name, value); + foreach (var prop in props) { + request.AddParameter(prop.Name, prop.Value, prop.Encode); } return request; diff --git a/src/RestSharp/Response/RestResponse.cs b/src/RestSharp/Response/RestResponse.cs index cc7843b0b..289a7a609 100644 --- a/src/RestSharp/Response/RestResponse.cs +++ b/src/RestSharp/Response/RestResponse.cs @@ -90,7 +90,7 @@ async Task GetDefaultResponse() { ContentType = httpResponse.Content.Headers.ContentType?.MediaType, ResponseStatus = calculateResponseStatus(httpResponse), ErrorException = httpResponse.MaybeException(), - ResponseUri = httpResponse.RequestMessage!.RequestUri, + ResponseUri = httpResponse.RequestMessage?.RequestUri, Server = httpResponse.Headers.Server.ToString(), StatusCode = httpResponse.StatusCode, StatusDescription = httpResponse.ReasonPhrase, diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index f317ed354..1d4d0dde8 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -49,6 +49,11 @@ public async Task ExecuteAsync(RestRequest request, CancellationTo async Task ExecuteInternal(RestRequest request, CancellationToken cancellationToken) { Ensure.NotNull(request, nameof(request)); + // Make sure we are not disposed of when someone tries to call us! + if (_disposed) { + throw new ObjectDisposedException(nameof(RestClient)); + } + using var requestContent = new RequestContent(this, request); if (Authenticator != null) await Authenticator.Authenticate(this, request).ConfigureAwait(false); diff --git a/src/RestSharp/RestClientExtensions.Json.cs b/src/RestSharp/RestClientExtensions.Json.cs index 1e4fa28f7..3f5a3cbbd 100644 --- a/src/RestSharp/RestClientExtensions.Json.cs +++ b/src/RestSharp/RestClientExtensions.Json.cs @@ -47,11 +47,13 @@ public static partial class RestClientExtensions { object parameters, CancellationToken cancellationToken = default ) { - var props = parameters.GetProperties(); + var props = parameters.GetProperties(); var request = new RestRequest(resource); - foreach (var (name, value) in props) { - Parameter parameter = resource.Contains($"{name}") ? new UrlSegmentParameter(name, value!) : new QueryParameter(name, value); + foreach (var prop in props) { + Parameter parameter = resource.Contains($"{prop.Name}") + ? new UrlSegmentParameter(prop.Name, prop.Value!, prop.Encode) + : new QueryParameter(prop.Name, prop.Value, prop.Encode); request.AddParameter(parameter); } @@ -141,4 +143,4 @@ public static async Task PutJsonAsync( var response = await client.PutAsync(restRequest, cancellationToken).ConfigureAwait(false); return response.StatusCode; } -} \ No newline at end of file +} diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 6553e6861..0cdde6fa9 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -10,7 +10,7 @@ - + diff --git a/test/RestSharp.Tests/ParametersTests.cs b/test/RestSharp.Tests/ParametersTests.cs index 8bfc6edf3..719e96ed5 100644 --- a/test/RestSharp.Tests/ParametersTests.cs +++ b/test/RestSharp.Tests/ParametersTests.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.IO; namespace RestSharp.Tests; @@ -26,10 +27,49 @@ public void AddDefaultHeadersUsingDictionary() { public void AddUrlSegmentWithInt() { const string name = "foo"; + var request = new RestRequest().AddUrlSegment(name, 1); var actual = request.Parameters.FirstOrDefault(x => x.Name == name); var expected = new UrlSegmentParameter(name, "1"); expected.Should().BeEquivalentTo(actual); } + + [Fact] + public void AddUrlSegmentModifiesUrlSegmentWithInt() { + const string name = "foo"; + var pathTemplate = "/{0}/resource"; + var path = String.Format(pathTemplate, "{" + name + "}"); + var urlSegmentValue = 1; + + var request = new RestRequest(path).AddUrlSegment(name, urlSegmentValue); + + var expected = String.Format(pathTemplate, urlSegmentValue); + + var client = new RestClient(BaseUrl); + + var actual = client.BuildUri(request).AbsolutePath; + + + expected.Should().BeEquivalentTo(actual); + } + + [Fact] + public void AddUrlSegmentModifiesUrlSegmentWithString() { + const string name = "foo"; + var pathTemplate = "/{0}/resource"; + var path = String.Format(pathTemplate, "{" + name + "}"); + var urlSegmentValue = "bar"; + + var request = new RestRequest(path).AddUrlSegment(name, urlSegmentValue); + + var expected = String.Format(pathTemplate, urlSegmentValue); + + var client = new RestClient(BaseUrl); + + var actual = client.BuildUri(request).AbsolutePath; + + expected.Should().BeEquivalentTo(actual); + + } } \ No newline at end of file