diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a0c9105..2233a81 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,10 +12,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET + - name: Setup .NET 8, 9 & 10 uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x - name: Restore dependencies run: dotnet restore diff --git a/Web.Test/RequestTest.cs b/Web.Test/RequestTest.cs index c89c39b..851092a 100644 --- a/Web.Test/RequestTest.cs +++ b/Web.Test/RequestTest.cs @@ -104,6 +104,24 @@ public async Task SendWebpImageRequest() await SendImageRequest("image/webp", "webp"); } + [TestMethod] + public void CheckHeaders() + { + Request request = new(""); + request + .AddHeader("Authorization", "test1") + .SetAuthorization("test2") + .SetAuthorization("test3"); + + KeyValuePair? authorizationHeader = request + .RequestHeaders + .Where(rh => rh.Key == "Authorization") + .FirstOrDefault(); + + Assert.IsNotNull(authorizationHeader, "Authorization header should be set"); + Assert.AreEqual("test3", authorizationHeader.Value.Value, "Authorization header value should be: test3"); + } + private async Task SendImageRequest(string acceptType, string imageType) { Request request = new(TestGetWebpImageUrl, HttpMethod.Get); diff --git a/Web/API/Request.cs b/Web/API/Request.cs index f1356af..6d32779 100644 --- a/Web/API/Request.cs +++ b/Web/API/Request.cs @@ -6,253 +6,298 @@ namespace FrApp42.Web.API { - public class Request - { - #region Variables + public class Request + { + #region Variables - /// - /// The HTTP client used to send requests. - /// - private readonly HttpClient _httpClient = new(); + /// + /// The HTTP client used to send requests. + /// + private readonly HttpClient _httpClient = new(); + + /// + /// Settings for JSON serialization. + /// + private readonly JsonSerializerSettings _jsonSerializerSettings = new() + { + ContractResolver = new JsonPropAttrResolver(), + Formatting = Formatting.Indented + }; /// - /// Settings for JSON serialization. + /// Set body as JSON /// - private readonly JsonSerializerSettings _jsonSerializerSettings = new() - { - ContractResolver = new JsonPropAttrResolver(), - Formatting = Formatting.Indented - }; + private bool _isJsonBody = false; /// /// Gets the URL to send the request to. /// public string URL { get; private set; } = string.Empty; - /// - /// Gets the HTTP method to use for the request. - /// - public HttpMethod Method { get; private set; } = HttpMethod.Get; - - /// - /// Gets the request headers. - /// - public Dictionary RequestHeaders { get; private set; } = []; + /// + /// Gets the HTTP method to use for the request. + /// + public HttpMethod Method { get; private set; } = HttpMethod.Get; - /// - /// Gets the content headers. - /// - public Dictionary ContentHeaders { get; private set; } = []; + /// + /// Gets the request headers. + /// + public Dictionary RequestHeaders { get; private set; } = []; - /// - /// Gets the query parameters. - /// - public Dictionary QueryParams { get; private set; } = []; + /// + /// Gets the content headers. + /// + public Dictionary ContentHeaders { get; private set; } = []; - /// - /// Gets the JSON body content of the request. - /// - public object Body { get; private set; } = null; + /// + /// Gets the query parameters. + /// + public Dictionary QueryParams { get; private set; } = []; - /// - /// Gets the binary document content of the request. - /// - public byte[] DocumentBody { get; private set; } = null; + /// + /// Gets the JSON body content of the request. + /// + public object Body { get; private set; } = null; - /// - /// Gets the name of the binary document file. - /// - public string? DocumentFileName { get; private set; } = null; + /// + /// Gets the binary document content of the request. + /// + public byte[] DocumentBody { get; private set; } = null; - /// - /// Gets the content type of the request. - /// - public string ContentType { get; private set; } = null; - - #endregion + /// + /// Gets the name of the binary document file. + /// + public string? DocumentFileName { get; private set; } = null; - #region Constructors + /// + /// Gets the content type of the request. + /// + public string ContentType { get; private set; } = null; + + #endregion - /// - /// Initializes a new instance of the Request class with the specified URL. - /// - /// The URL to send the request to. - public Request(string url) - { - URL = url; - } + #region Constructors - /// - /// Initializes a new instance of the Request class with the specified URL and HTTP method. - /// - /// The URL to send the request to. - /// The HTTP method to use for the request. - public Request(string url, HttpMethod method): this(url) - { - Method = method; - } + /// + /// Initializes a new instance of the Request class with the specified URL. + /// + /// The URL to send the request to. + public Request(string url) + { + URL = url; + } - #endregion + /// + /// Initializes a new instance of the Request class with the specified URL and HTTP method. + /// + /// The URL to send the request to. + /// The HTTP method to use for the request. + public Request(string url, HttpMethod method): this(url) + { + Method = method; + } - #region Functions to update variables + #endregion - /// - /// Adds a header to the request. - /// - /// The header key. - /// The header value. - /// Instance - public Request AddHeader(string key, string value) - { - RequestHeaders.Add(key, value); + #region Functions to update variables - return this; - } + /// + /// Adds a header to the request. + /// + /// The header key. + /// The header value. + /// Instance + public Request AddHeader(string key, string value) + { + RequestHeaders.Add(key, value); + + return this; + } - /// - /// Adds multiple headers from a dictionary of key-value pairs to the request. - /// - /// Headers in dictionary. - /// Instance - public Request AddHeaders(Dictionary headers) - { - foreach (KeyValuePair header in headers) - { - RequestHeaders.Add(header.Key, header.Value); - } + /// + /// Adds multiple headers from a dictionary of key-value pairs to the request. + /// + /// Headers in dictionary. + /// Instance + public Request AddHeaders(Dictionary headers) + { + foreach (KeyValuePair header in headers) + { + RequestHeaders.Add(header.Key, header.Value); + } - return this; - } + return this; + } - /// - /// Adds a content header to the request - /// - /// The content header key. - /// The content header value. - /// Instance - public Request AddContentHeader(string key, string value) - { - ContentHeaders.Add(key, value); + /// + /// Sets the authorization header. + /// In case of an existing Authorization header, its value + /// will be replaced with the last set. + /// + /// Authorization header value + /// Instance + public Request SetAuthorization(string value) + { + KeyValuePair? existingAuthorization = RequestHeaders + .Where(rh => rh.Key == "Authorization") + .FirstOrDefault(); + + if (existingAuthorization.Equals(new KeyValuePair())) + { + AddHeader("Authorization", value); + } + else + { + RequestHeaders[existingAuthorization.Value.Key] = value; + } - return this; - } + return this; + } - /// - /// Adds multiple content headers from a dictionary of key-value pairs to the request. - /// - /// Content headers in dictionary. - /// Instance - public Request AddContentHeaders(Dictionary headers) - { - foreach (KeyValuePair header in headers) - { - ContentHeaders.Add(header.Key, header.Value); - } + /// + /// Adds a content header to the request + /// + /// The content header key. + /// The content header value. + /// Instance + public Request AddContentHeader(string key, string value) + { + ContentHeaders.Add(key, value); + + return this; + } - return this; - } + /// + /// Adds multiple content headers from a dictionary of key-value pairs to the request. + /// + /// Content headers in dictionary. + /// Instance + public Request AddContentHeaders(Dictionary headers) + { + foreach (KeyValuePair header in headers) + { + ContentHeaders.Add(header.Key, header.Value); + } - /// - /// Adds a query parameter to the request. - /// - /// The query parameter key. - /// The query parameter value. - /// Instance - public Request AddQueryParam(string key, string value) - { - QueryParams.Add(key, value); + return this; + } - return this; - } + /// + /// Adds a query parameter to the request. + /// + /// The query parameter key. + /// The query parameter value. + /// Instance + public Request AddQueryParam(string key, string value) + { + QueryParams.Add(key, value); + + return this; + } - /// - /// Adds multiple content headers from a dictionary of key-value pairs to the request. - /// - /// Query parameters in dictionary. - /// Instance - public Request AddQueryParams(Dictionary queryParams) - { - foreach (KeyValuePair query in queryParams) - { - QueryParams.Add(query.Key, query.Value); - } + /// + /// Adds multiple content headers from a dictionary of key-value pairs to the request. + /// + /// Query parameters in dictionary. + /// Instance + public Request AddQueryParams(Dictionary queryParams) + { + foreach (KeyValuePair query in queryParams) + { + QueryParams.Add(query.Key, query.Value); + } - return this; - } + return this; + } - /// - /// Adds an "Accept" header with the value "application/json" to the request. - /// - /// Instance - public Request AcceptJson() - { - RequestHeaders.Add("Accept", "application/json"); + /// + /// Adds an "Accept" header with the value "application/json" to the request. + /// + /// Instance + public Request AcceptJson() + { + RequestHeaders.Add("Accept", "application/json"); - return this; - } + return this; + } - /// - /// Adds a JSON body to the request content. - /// - /// The JSON body content. - /// Instance - public Request AddJsonBody(object body) - { - Body = body; + /// + /// Adds a JSON body to the request content. + /// + /// The JSON body content. + /// Instance + public Request AddJsonBody(object body) + { + Body = body; + _isJsonBody = true; return this; - } + } - /// - /// Adds a to the request content. - /// - /// The binary file content. - /// The name of the file. - /// Instance - public Request AddByteBody(byte[] document, string? fileName) - { - DocumentBody = document; - DocumentFileName = fileName; + /// + /// Adds a to the request content. + /// + /// The binary file content. + /// The name of the file. + /// Instance + public Request AddByteBody(byte[] document, string? fileName) + { + DocumentBody = document; + DocumentFileName = fileName; + + return this; + } - return this; - } + public Request AddTextBody(object body) + { + Body = body; + _isJsonBody = false; + return this; + } - /// - /// Sets the content type of the request. - /// - /// The content type of the request. - /// Instance - public Request SetContentType(string contentType) - { - ContentType = contentType; + /// + /// Sets the content type of the request. + /// + /// The content type of the request. + /// Instance + public Request SetContentType(string contentType) + { + ContentType = contentType; - return this; - } + return this; + } - #endregion + #endregion - #region Peform request + #region Peform request - /// - /// Executes the HTTP request with the JSON body content included. - /// - /// The type of the response expected from the request. - /// A Result object containing the response. - public async Task> Run() - { - HttpRequestMessage request = BuildBaseRequest(); + /// + /// Executes the HTTP request with the JSON body content included. + /// + /// The type of the response expected from the request. + /// A Result object containing the response. + public async Task> Run() + { + HttpRequestMessage request = BuildBaseRequest(); - if (Body != null) - { - string json = JsonConvert.SerializeObject(Body, _jsonSerializerSettings); - request.Content = new StringContent(json, Encoding.UTF8, "application/json"); - request.Content.Headers.ContentType = new("application/json"); - } + if (Body != null) + { + if (_isJsonBody) + { + string json = JsonConvert.SerializeObject(Body, _jsonSerializerSettings); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + request.Content.Headers.ContentType = new("application/json"); + } else + { + request.Content = new StringContent((string)Body, Encoding.UTF8, "text/plain"); + request.Content.Headers.ContentType = new("text/plain"); + } + } - Result result = await Process(request); + Result result = await Process(request); - return result; - } + return result; + } /// /// Executes the HTTP request and returns the response content as a byte array. @@ -260,36 +305,36 @@ public async Task> Run() /// The type of the response expected from the request. /// A Result object containing the response. public async Task> RunDocument() - { - HttpRequestMessage request = BuildBaseRequest(); + { + HttpRequestMessage request = BuildBaseRequest(); - if (DocumentBody == null) - { - return new Result() - { - Error = "Document cannot be null", - StatusCode = 500, - }; - } + if (DocumentBody == null) + { + return new Result() + { + Error = "Document cannot be null", + StatusCode = 500, + }; + } - MultipartFormDataContent content = new(); - ByteArrayContent fileContent = new(DocumentBody); + MultipartFormDataContent content = new(); + ByteArrayContent fileContent = new(DocumentBody); - fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("binary") - { - Name = "file", - FileName = DocumentFileName - }; + fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("binary") + { + Name = "file", + FileName = DocumentFileName + }; - content.Add(fileContent); + content.Add(fileContent); - request.Content = content; - request.Content.Headers.ContentType = new(ContentType); + request.Content = content; + request.Content.Headers.ContentType = new(ContentType); - Result result = await Process(request); + Result result = await Process(request); - return result; - } + return result; + } /// /// Executes the HTTP request that will return a byte array. @@ -299,15 +344,15 @@ public async Task> RunDocument() /// the status code of the response, and any error message if the request fails. /// public async Task> RunGetBytes() - { + { HttpRequestMessage request = BuildBaseRequest(); Result result = new(); - try - { - HttpResponseMessage response = await _httpClient.SendAsync(request); + try + { + HttpResponseMessage response = await _httpClient.SendAsync(request); - result.StatusCode = (int)response.StatusCode; + result.StatusCode = (int)response.StatusCode; if (response.IsSuccessStatusCode) { @@ -318,155 +363,155 @@ public async Task> RunGetBytes() result.Error = await response.Content.ReadAsStringAsync(); } } - catch (Exception ex) - { + catch (Exception ex) + { result.StatusCode = 500; result.Error = ex.Message; } - return result; + return result; } - /// - /// Executes the HTTP request by sending the raw binary content directly in the request body, - /// without using multipart encoding. This method is ideal for APIs expecting direct binary content, - /// equivalent to Postman's "Binary" body type. - /// - /// The type of the expected response. - /// - /// A object containing the deserialized response, the HTTP status code, - /// and any error message if the request fails. - /// - public async Task> RunBinaryRaw() - { - HttpRequestMessage request = BuildBaseRequest(); - - if (DocumentBody == null) - { - return new Result - { - Error = "Document cannot be null", - StatusCode = 500 - }; - } - - request.Content = new ByteArrayContent(DocumentBody); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(ContentType ?? "application/octet-stream"); - - return await Process(request); - } - - #endregion - - #region Private methods + /// + /// Executes the HTTP request by sending the raw binary content directly in the request body, + /// without using multipart encoding. This method is ideal for APIs expecting direct binary content, + /// equivalent to Postman's "Binary" body type. + /// + /// The type of the expected response. + /// + /// A object containing the deserialized response, the HTTP status code, + /// and any error message if the request fails. + /// + public async Task> RunBinaryRaw() + { + HttpRequestMessage request = BuildBaseRequest(); - /// - /// Constructs the URL for the request, including any query parameters if present. - /// - /// Request URL with added query parameters. - private string BuildUrl() - { - StringBuilder builder = new(URL); - string fullUrl = URL; - - if (fullUrl.EndsWith("/")) - builder.Remove(fullUrl.Length - 1, 1); - - if (QueryParams.Count() > 0) - { - builder.Append("?"); - - for (int i = 0; i < QueryParams.Count(); i++) - { - KeyValuePair query = QueryParams.ElementAt(i); - builder.Append($"{query.Key}={query.Value}"); - - if (!(i == QueryParams.Count() - 1)) - builder.Append("&"); - } - } + if (DocumentBody == null) + { + return new Result + { + Error = "Document cannot be null", + StatusCode = 500 + }; + } - return builder.ToString(); - } + request.Content = new ByteArrayContent(DocumentBody); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(ContentType ?? "application/octet-stream"); - /// - /// Constructs the base HTTP request message by adding the specified headers. - /// - /// Base request message. - private HttpRequestMessage BuildBaseRequest() - { - HttpRequestMessage request = new(Method, BuildUrl()); + return await Process(request); + } - for (int i = 0; i < RequestHeaders.Count(); i++) - { - KeyValuePair header = RequestHeaders.ElementAt(i); - request.Headers.Add(header.Key, header.Value); - } + #endregion - return request; - } + #region Private methods - /// - /// Executes the HTTP request and processes the response. - /// - /// The type of the response expected from the request. - /// The prepared HTTP request message. - /// A Result object containing the response. - private async Task> Process(HttpRequestMessage request) - { - HttpResponseMessage response; - Result result = new(); - - try - { - response = await _httpClient.SendAsync(request); - result.StatusCode = (int)response.StatusCode; - - if ( - response.Content == null || - string.IsNullOrEmpty(await response.Content.ReadAsStringAsync()) - ) - { - result.Value = default; - - return result; - } + /// + /// Constructs the URL for the request, including any query parameters if present. + /// + /// Request URL with added query parameters. + private string BuildUrl() + { + StringBuilder builder = new(URL); + string fullUrl = URL; - if (response.IsSuccessStatusCode) - { - string? mediaType = response.Content?.Headers?.ContentType?.MediaType.ToLower(); - string contentResponse = await response.Content?.ReadAsStringAsync(); - - switch (true) - { - case bool b when (mediaType.Contains("xml")): - XmlSerializer xmlSerializer = new(typeof(T)); - StringReader reader = new(contentResponse); - - result.Value = (T)xmlSerializer.Deserialize(reader); - break; - case bool b when (mediaType.Contains("application/json")): - result.Value = JsonConvert.DeserializeObject(contentResponse); - break; - default: - result.Value = default; - break; - } - } - else - { - result.Error = await response.Content.ReadAsStringAsync(); - } - } - catch (Exception ex) - { - result.StatusCode = 500; - result.Error = ex.Message; - } - - return result; - } - - #endregion - } + if (fullUrl.EndsWith("/")) + builder.Remove(fullUrl.Length - 1, 1); + + if (QueryParams.Count() > 0) + { + builder.Append("?"); + + for (int i = 0; i < QueryParams.Count(); i++) + { + KeyValuePair query = QueryParams.ElementAt(i); + builder.Append($"{query.Key}={query.Value}"); + + if (!(i == QueryParams.Count() - 1)) + builder.Append("&"); + } + } + + return builder.ToString(); + } + + /// + /// Constructs the base HTTP request message by adding the specified headers. + /// + /// Base request message. + private HttpRequestMessage BuildBaseRequest() + { + HttpRequestMessage request = new(Method, BuildUrl()); + + for (int i = 0; i < RequestHeaders.Count(); i++) + { + KeyValuePair header = RequestHeaders.ElementAt(i); + request.Headers.Add(header.Key, header.Value); + } + + return request; + } + + /// + /// Executes the HTTP request and processes the response. + /// + /// The type of the response expected from the request. + /// The prepared HTTP request message. + /// A Result object containing the response. + private async Task> Process(HttpRequestMessage request) + { + HttpResponseMessage response; + Result result = new(); + + try + { + response = await _httpClient.SendAsync(request); + result.StatusCode = (int)response.StatusCode; + + if ( + response.Content == null || + string.IsNullOrEmpty(await response.Content.ReadAsStringAsync()) + ) + { + result.Value = default; + + return result; + } + + if (response.IsSuccessStatusCode) + { + string? mediaType = response.Content?.Headers?.ContentType?.MediaType.ToLower(); + string contentResponse = await response.Content?.ReadAsStringAsync(); + + switch (true) + { + case bool b when (mediaType.Contains("xml")): + XmlSerializer xmlSerializer = new(typeof(T)); + StringReader reader = new(contentResponse); + + result.Value = (T)xmlSerializer.Deserialize(reader); + break; + case bool b when (mediaType.Contains("application/json")): + result.Value = JsonConvert.DeserializeObject(contentResponse); + break; + default: + result.Value = default; + break; + } + } + else + { + result.Error = await response.Content.ReadAsStringAsync(); + } + } + catch (Exception ex) + { + result.StatusCode = 500; + result.Error = ex.Message; + } + + return result; + } + + #endregion + } } diff --git a/Web/Web.csproj b/Web/Web.csproj index 8f87d55..d13234e 100644 --- a/Web/Web.csproj +++ b/Web/Web.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0;net9.0;net10.0 enable enable FrApp42.$(MSBuildProjectName) @@ -20,10 +20,11 @@ README.md FrenchyApps42 logo.png - 1.3.0 + 1.4.0 + \ True @@ -39,7 +40,7 @@ - +