diff --git a/Source/EasyNetQ.Management.Client.IntegrationTests/ManagementClientTests.cs b/Source/EasyNetQ.Management.Client.IntegrationTests/ManagementClientTests.cs index ee184109..29504a73 100644 --- a/Source/EasyNetQ.Management.Client.IntegrationTests/ManagementClientTests.cs +++ b/Source/EasyNetQ.Management.Client.IntegrationTests/ManagementClientTests.cs @@ -89,8 +89,9 @@ public async Task Should_be_able_to_close_connection() [Fact] public async Task Should_be_able_to_configure_request() { - var client = new ManagementClient(fixture.Host, fixture.User, fixture.Password, configureRequest: - req => req.Headers.Add("x-not-used", "some_value")); + var client = new ManagementClient( + fixture.Host, fixture.User, fixture.Password, configureHttpRequestMessage: req => req.Headers.Add("x-not-used", "some_value") + ); await client.GetOverviewAsync(); } diff --git a/Source/EasyNetQ.Management.Client.Tests/ManagementClientConstructorTests.cs b/Source/EasyNetQ.Management.Client.Tests/ManagementClientConstructorTests.cs index 2edff33f..0cb2601c 100644 --- a/Source/EasyNetQ.Management.Client.Tests/ManagementClientConstructorTests.cs +++ b/Source/EasyNetQ.Management.Client.Tests/ManagementClientConstructorTests.cs @@ -45,20 +45,4 @@ public void Host_url_should_be_legal(string url, bool isValid) exception.Message.Should().Be("hostUrl is illegal"); } } - - [Fact] - public void Username_should_not_be_null() - { - var exception = Assert.Throws(() => new ManagementClient("localhost", string.Empty, "password")); - - exception.Message.Should().Be("username is null or empty"); - } - - [Fact] - public void Password_should_not_be_null() - { - var exception = Assert.Throws(() => new ManagementClient("localhost", "user", string.Empty)); - - exception.Message.Should().Be("password is null or empty"); - } } diff --git a/Source/EasyNetQ.Management.Client/IManagementClient.cs b/Source/EasyNetQ.Management.Client/IManagementClient.cs index 399b0ebb..19284a72 100755 --- a/Source/EasyNetQ.Management.Client/IManagementClient.cs +++ b/Source/EasyNetQ.Management.Client/IManagementClient.cs @@ -7,17 +7,7 @@ public interface IManagementClient : IDisposable /// /// The host URL that this instance is using. /// - string HostUrl { get; } - - /// - /// The Username that this instance is connecting as. - /// - string Username { get; } - - /// - /// The port number this instance connects using. - /// - int PortNumber { get; } + Uri Endpoint { get; } /// /// Various random bits of information that describe the whole system. diff --git a/Source/EasyNetQ.Management.Client/Internals/QueryStringHelpers.cs b/Source/EasyNetQ.Management.Client/Internals/QueryStringHelpers.cs new file mode 100644 index 00000000..aa3d4876 --- /dev/null +++ b/Source/EasyNetQ.Management.Client/Internals/QueryStringHelpers.cs @@ -0,0 +1,25 @@ +using System.Text; + +namespace EasyNetQ.Management.Client.Internals; + +internal static class QueryStringHelpers +{ + public static string AddQueryString(string uri, IReadOnlyDictionary? queryString) + { + if (queryString == null || queryString.Count == 0) return uri; + + var queryIndex = uri.IndexOf('?'); + var hasQuery = queryIndex != -1; + var sb = new StringBuilder(); + sb.Append(uri); + foreach (var parameter in queryString) + { + sb.Append(hasQuery ? '&' : '?'); + sb.Append(Uri.EscapeDataString(parameter.Key)); + sb.Append('='); + sb.Append(Uri.EscapeDataString(parameter.Value)); + hasQuery = true; + } + return sb.ToString(); + } +} diff --git a/Source/EasyNetQ.Management.Client/Internals/RelativePath.cs b/Source/EasyNetQ.Management.Client/Internals/RelativePath.cs index 642f865b..de701584 100644 --- a/Source/EasyNetQ.Management.Client/Internals/RelativePath.cs +++ b/Source/EasyNetQ.Management.Client/Internals/RelativePath.cs @@ -6,9 +6,10 @@ internal readonly struct RelativePath public RelativePath(string segment) => segments = new[] { segment }; - public string BuildEscaped() => string.Join("/", (segments ?? Array.Empty()).Select(Uri.EscapeDataString)); + public string Build() => string.Join("/", (segments ?? Array.Empty()).Select(Uri.EscapeDataString)); public static RelativePath operator /(RelativePath parent, string segment) => parent.Add(segment); + public static RelativePath operator /(RelativePath parent, char segment) => parent.Add(segment + ""); private RelativePath(string[] segments) => this.segments = segments; diff --git a/Source/EasyNetQ.Management.Client/LegacyEndpointBuilder.cs b/Source/EasyNetQ.Management.Client/LegacyEndpointBuilder.cs new file mode 100644 index 00000000..8ee78b98 --- /dev/null +++ b/Source/EasyNetQ.Management.Client/LegacyEndpointBuilder.cs @@ -0,0 +1,32 @@ +using System.Text.RegularExpressions; + +namespace EasyNetQ.Management.Client; + +internal static class LegacyEndpointBuilder +{ + private static readonly Regex UrlRegex = new(@"^(http|https):\/\/\[?.+\w\]?$", RegexOptions.Compiled | RegexOptions.Singleline); + + public static Uri Build(string hostUrl, int portNumber, bool ssl) + { + if (string.IsNullOrEmpty(hostUrl)) throw new ArgumentException("hostUrl is null or empty"); + + if (hostUrl.StartsWith("https://")) ssl = true; + + if (ssl) + { + if (hostUrl.Contains("http://")) throw new ArgumentException("hostUrl is illegal"); + hostUrl = hostUrl.Contains("https://") ? hostUrl : "https://" + hostUrl; + } + else + { + if (hostUrl.Contains("https://")) throw new ArgumentException("hostUrl is illegal"); + hostUrl = hostUrl.Contains("http://") ? hostUrl : "http://" + hostUrl; + } + + if (!UrlRegex.IsMatch(hostUrl)) throw new ArgumentException("hostUrl is illegal"); + + return Uri.TryCreate(portNumber != 443 ? $"{hostUrl}:{portNumber}" : hostUrl, UriKind.Absolute, out var uri) + ? uri + : throw new ArgumentException("hostUrl is illegal"); + } +} diff --git a/Source/EasyNetQ.Management.Client/ManagementClient.cs b/Source/EasyNetQ.Management.Client/ManagementClient.cs index 31a71757..2f4a40cd 100644 --- a/Source/EasyNetQ.Management.Client/ManagementClient.cs +++ b/Source/EasyNetQ.Management.Client/ManagementClient.cs @@ -1,7 +1,5 @@ using System.Net; using System.Net.Http.Headers; -using System.Text; -using System.Text.RegularExpressions; using EasyNetQ.Management.Client.Internals; using EasyNetQ.Management.Client.Model; using EasyNetQ.Management.Client.Serialization; @@ -13,36 +11,33 @@ namespace EasyNetQ.Management.Client; public class ManagementClient : IManagementClient { - private static readonly RelativePath Vhosts = new("vhosts"); - private static readonly RelativePath AlivenessTest = new("aliveness-test"); - private static readonly RelativePath Connections = new("connections"); - private static readonly RelativePath Consumers = new("consumers"); - private static readonly RelativePath Channels = new("channels"); - private static readonly RelativePath Users = new("users"); - private static readonly RelativePath Permissions = new("permissions"); - private static readonly RelativePath Parameters = new("parameters"); - private static readonly RelativePath Bindings = new("bindings"); - private static readonly RelativePath Queues = new("queues"); - private static readonly RelativePath Exchanges = new("exchanges"); - private static readonly RelativePath TopicPermissions = new("topic-permissions"); - private static readonly RelativePath Policies = new("policies"); - private static readonly RelativePath FederationLinks = new("federation-links"); - private static readonly RelativePath Overview = new("overview"); - private static readonly RelativePath Nodes = new("nodes"); - private static readonly RelativePath Definitions = new("definitions"); - - - private static readonly Regex ParameterNameRegex = new("([a-z])([A-Z])", RegexOptions.Compiled); + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(20); + + private static readonly RelativePath Api = new("api"); + private static readonly RelativePath Vhosts = Api / "vhosts"; + private static readonly RelativePath AlivenessTest = Api / "aliveness-test"; + private static readonly RelativePath Connections = Api / "connections"; + private static readonly RelativePath Consumers = Api / "consumers"; + private static readonly RelativePath Channels = Api / "channels"; + private static readonly RelativePath Users = Api / "users"; + private static readonly RelativePath Permissions = Api / "permissions"; + private static readonly RelativePath Parameters = Api / "parameters"; + private static readonly RelativePath Bindings = Api / "bindings"; + private static readonly RelativePath Queues = Api / "queues"; + private static readonly RelativePath Exchanges = Api / "exchanges"; + private static readonly RelativePath TopicPermissions = Api / "topic-permissions"; + private static readonly RelativePath Policies = Api / "policies"; + private static readonly RelativePath FederationLinks = Api / "federation-links"; + private static readonly RelativePath Overview = Api / "overview"; + private static readonly RelativePath Nodes = Api / "nodes"; + private static readonly RelativePath Definitions = Api / "definitions"; private static readonly MediaTypeWithQualityHeaderValue JsonMediaTypeHeaderValue = new("application/json"); public static readonly JsonSerializerSettings Settings; - private readonly Action configureRequest; - private readonly TimeSpan defaultTimeout = TimeSpan.FromSeconds(20); private readonly HttpClient httpClient; - - private readonly Regex urlRegex = new(@"^(http|https):\/\/\[?.+\w\]?$", RegexOptions.Compiled | RegexOptions.Singleline); + private readonly Action? configureHttpRequestMessage; static ManagementClient() { @@ -62,76 +57,47 @@ static ManagementClient() Settings.Converters.Add(new HaParamsConverter()); } - public string HostUrl { get; } - - public string Username { get; } - - public int PortNumber { get; } - + [Obsolete("Please use another constructor")] public ManagementClient( string hostUrl, string username, string password, int portNumber = 15672, TimeSpan? timeout = null, - Action? configureRequest = null, + Action? configureHttpRequestMessage = null, bool ssl = false, - Action? handlerConfigurator = null + Action? configureHttpClientHandler = null + ) : this( + LegacyEndpointBuilder.Build(hostUrl, portNumber, ssl), + username, + password, + timeout, + configureHttpRequestMessage, + configureHttpClientHandler ) { - if (string.IsNullOrEmpty(hostUrl)) - { - throw new ArgumentException("hostUrl is null or empty"); - } - - if (hostUrl.StartsWith("https://")) - ssl = true; - - if (ssl) - { - if (hostUrl.Contains("http://")) - throw new ArgumentException("hostUrl is illegal"); - hostUrl = hostUrl.Contains("https://") ? hostUrl : "https://" + hostUrl; - } - else - { - if (hostUrl.Contains("https://")) - throw new ArgumentException("hostUrl is illegal"); - hostUrl = hostUrl.Contains("http://") ? hostUrl : "http://" + hostUrl; - } - - if (!urlRegex.IsMatch(hostUrl) || !Uri.TryCreate(hostUrl, UriKind.Absolute, out var urlUri)) - { - throw new ArgumentException("hostUrl is illegal"); - } - - if (string.IsNullOrEmpty(username)) - { - throw new ArgumentException("username is null or empty"); - } - - if (string.IsNullOrEmpty(password)) - { - throw new ArgumentException("password is null or empty"); - } - - configureRequest ??= _ => { }; - - HostUrl = hostUrl; - Username = username; - PortNumber = portNumber; - this.configureRequest = configureRequest; + } - var httpHandler = new HttpClientHandler - { - Credentials = new NetworkCredential(username, password) - }; + public ManagementClient( + Uri endpoint, + string username, + string password, + TimeSpan? timeout = null, + Action? configureHttpRequestMessage = null, + Action? configureHttpClientHandler = null + ) + { + if (!endpoint.IsAbsoluteUri) throw new ArgumentOutOfRangeException(nameof(endpoint), endpoint, "Endpoint should be absolute"); - handlerConfigurator?.Invoke(httpHandler); + this.configureHttpRequestMessage = configureHttpRequestMessage; - httpClient = new HttpClient(httpHandler) { Timeout = timeout ?? defaultTimeout }; + var httpHandler = new HttpClientHandler { Credentials = new NetworkCredential(username, password) }; + configureHttpClientHandler?.Invoke(httpHandler); + httpClient = new HttpClient(httpHandler) { Timeout = timeout ?? DefaultTimeout, BaseAddress = endpoint }; } + public Uri Endpoint => httpClient.BaseAddress!; + public Task GetOverviewAsync( GetLengthsCriteria? lengthsCriteria = null, GetRatesCriteria? ratesCriteria = null, @@ -586,7 +552,7 @@ private async Task GetAsync( CancellationToken cancellationToken = default ) { - using var request = CreateRequestForPath(HttpMethod.Get, path.BuildEscaped(), BuildQueryString(queryParameters)); + using var request = CreateRequestForPath(HttpMethod.Get, path, queryParameters); using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); return await DeserializeResponseAsync(c => c == HttpStatusCode.OK, response).ConfigureAwait(false); @@ -598,7 +564,7 @@ private async Task PostAsync( CancellationToken cancellationToken = default ) { - using var request = CreateRequestForPath(HttpMethod.Post, path.BuildEscaped(), string.Empty); + using var request = CreateRequestForPath(HttpMethod.Post, path); InsertRequestBody(request, item); @@ -611,7 +577,7 @@ private async Task PostAsync( private async Task DeleteAsync(RelativePath path, CancellationToken cancellationToken = default) { - using var request = CreateRequestForPath(HttpMethod.Delete, path.BuildEscaped(), string.Empty); + using var request = CreateRequestForPath(HttpMethod.Delete, path); using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); await DeserializeResponseAsync(c => c == HttpStatusCode.NoContent, response).ConfigureAwait(false); @@ -623,7 +589,7 @@ private async Task PutAsync( CancellationToken cancellationToken = default ) where T : class { - using var request = CreateRequestForPath(HttpMethod.Put, path.BuildEscaped(), string.Empty); + using var request = CreateRequestForPath(HttpMethod.Put, path); if (item != default) InsertRequestBody(request, item); @@ -661,41 +627,17 @@ private static void InsertRequestBody(HttpRequestMessage request, T item) request.Content = content; } - private HttpRequestMessage CreateRequestForPath(HttpMethod httpMethod, string path, string query) - { - string uriString; - if (PortNumber != 443) - uriString = $"{HostUrl}:{PortNumber}/api/{path}{query}"; - else - uriString = $"{HostUrl}/api/{path}{query}"; - - var uri = new Uri(uriString); - var request = new HttpRequestMessage(httpMethod, uri); - configureRequest(request); - return request; - } - - private static string BuildQueryString(IReadOnlyDictionary? queryParameters) + private HttpRequestMessage CreateRequestForPath( + HttpMethod httpMethod, + in RelativePath path, + IReadOnlyDictionary? queryParameters = null + ) { - if (queryParameters == null || queryParameters.Count == 0) - return string.Empty; - - var queryStringBuilder = new StringBuilder("?"); - var first = true; - foreach (var parameter in queryParameters) - { - if (!first) - queryStringBuilder.Append('&'); - var name = ParameterNameRegex.Replace(parameter.Key, "$1_$2").ToLower(); - var value = parameter.Value; - queryStringBuilder.Append($"{name}={value}"); - first = false; - } - - return queryStringBuilder.ToString(); + var httpRequestMessage = new HttpRequestMessage(httpMethod, QueryStringHelpers.AddQueryString(path.Build(), queryParameters)); + configureHttpRequestMessage?.Invoke(httpRequestMessage); + return httpRequestMessage; } - private static IReadOnlyDictionary? MergeQueryParameters(params IReadOnlyDictionary?[]? multipleQueryParameters) { if (multipleQueryParameters == null || multipleQueryParameters.Length == 0) diff --git a/Source/EasyNetQ.Management.Client/Model/GetLengthsCriteria.cs b/Source/EasyNetQ.Management.Client/Model/GetLengthsCriteria.cs index 7d2e0b21..f6b1b85a 100644 --- a/Source/EasyNetQ.Management.Client/Model/GetLengthsCriteria.cs +++ b/Source/EasyNetQ.Management.Client/Model/GetLengthsCriteria.cs @@ -12,6 +12,7 @@ public GetLengthsCriteria(int age, int increment) LengthsAge = age; LengthsIncr = increment; } + public int LengthsAge { get; private set; } public int LengthsIncr { get; private set; } @@ -19,8 +20,8 @@ public IReadOnlyDictionary ToQueryParameters() { return new Dictionary { - {nameof(LengthsAge), LengthsAge.ToString()}, - {nameof(LengthsIncr), LengthsIncr.ToString()} + { "lengths_age", LengthsAge.ToString() }, + { "lengths_incr", LengthsIncr.ToString() } }; } } diff --git a/Source/EasyNetQ.Management.Client/Model/GetRatesCriteria.cs b/Source/EasyNetQ.Management.Client/Model/GetRatesCriteria.cs index 182516bc..25cabdf0 100644 --- a/Source/EasyNetQ.Management.Client/Model/GetRatesCriteria.cs +++ b/Source/EasyNetQ.Management.Client/Model/GetRatesCriteria.cs @@ -20,8 +20,8 @@ public IReadOnlyDictionary ToQueryParameters() { return new Dictionary { - {nameof(MsgRatesAge), MsgRatesAge.ToString()}, - {nameof(MsgRatesIncr), MsgRatesIncr.ToString()} + { "msg_rates_age", MsgRatesAge.ToString() }, + { "msg_rates_incr", MsgRatesIncr.ToString() } }; } }