Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept endpoint as Uri #185

Merged
merged 4 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentException>(() => 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<ArgumentException>(() => new ManagementClient("localhost", "user", string.Empty));

exception.Message.Should().Be("password is null or empty");
}
}
12 changes: 1 addition & 11 deletions Source/EasyNetQ.Management.Client/IManagementClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,7 @@ public interface IManagementClient : IDisposable
/// <summary>
/// The host URL that this instance is using.
/// </summary>
string HostUrl { get; }

/// <summary>
/// The Username that this instance is connecting as.
/// </summary>
string Username { get; }

/// <summary>
/// The port number this instance connects using.
/// </summary>
int PortNumber { get; }
Uri Endpoint { get; }

/// <summary>
/// Various random bits of information that describe the whole system.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Text;

namespace EasyNetQ.Management.Client.Internals;

internal static class QueryStringHelpers
{
public static string AddQueryString(string uri, IReadOnlyDictionary<string, string>? 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ internal readonly struct RelativePath

public RelativePath(string segment) => segments = new[] { segment };

public string BuildEscaped() => string.Join("/", (segments ?? Array.Empty<string>()).Select(Uri.EscapeDataString));
public string Build() => string.Join("/", (segments ?? Array.Empty<string>()).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;
Expand Down
32 changes: 32 additions & 0 deletions Source/EasyNetQ.Management.Client/LegacyEndpointBuilder.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
178 changes: 60 additions & 118 deletions Source/EasyNetQ.Management.Client/ManagementClient.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<HttpRequestMessage> 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<HttpRequestMessage>? configureHttpRequestMessage;

static ManagementClient()
{
Expand All @@ -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<HttpRequestMessage>? configureRequest = null,
Action<HttpRequestMessage>? configureHttpRequestMessage = null,
bool ssl = false,
Action<HttpClientHandler>? handlerConfigurator = null
Action<HttpClientHandler>? 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<HttpRequestMessage>? configureHttpRequestMessage = null,
Action<HttpClientHandler>? 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 };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider to switch to IHttpClientFactory to get all rich configuration features on caller side. So you do not bother with Action<HttpClientHandler> when manually building HttpClient. Of course this will be one more dependency.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine with that if there is a simple way to wire up IHttpClientFactory in console app without DI.

Copy link
Contributor

@sungam3r sungam3r Dec 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may use some DI stuff inside like for CreateBus in ENQ. Consider something like that:

public class ManagementClient
 {
   public ManagementClient(...., Action<IHttpClientBuilder> configure)
   {
     var services = new ServiceCollection();
     var builder = services.AddHttpClient(...);
     configure(builder); // so caller may use all existing available rich API to configure http pipeline
     provider = services.BuildServiceProvider(); // store into field, dispose field in Dispose() method
     httpClient = provider.GetRequiredService<IHttpClientFactory>().GetClient(nameof(ManagementClient));
    }
}

It another ctor accepting Action<IHttpClientBuilder> configure instead of IHttpClientFactory.

}

public Uri Endpoint => httpClient.BaseAddress!;

public Task<Overview> GetOverviewAsync(
GetLengthsCriteria? lengthsCriteria = null,
GetRatesCriteria? ratesCriteria = null,
Expand Down Expand Up @@ -586,7 +552,7 @@ private async Task<T> GetAsync<T>(
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<T>(c => c == HttpStatusCode.OK, response).ConfigureAwait(false);
Expand All @@ -598,7 +564,7 @@ private async Task<TResult> PostAsync<TItem, TResult>(
CancellationToken cancellationToken = default
)
{
using var request = CreateRequestForPath(HttpMethod.Post, path.BuildEscaped(), string.Empty);
using var request = CreateRequestForPath(HttpMethod.Post, path);

InsertRequestBody(request, item);

Expand All @@ -611,7 +577,7 @@ private async Task<TResult> PostAsync<TItem, TResult>(

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);
Expand All @@ -623,7 +589,7 @@ private async Task PutAsync<T>(
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);

Expand Down Expand Up @@ -661,41 +627,17 @@ private static void InsertRequestBody<T>(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<string, string>? queryParameters)
private HttpRequestMessage CreateRequestForPath(
HttpMethod httpMethod,
in RelativePath path,
IReadOnlyDictionary<string, string>? 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<string, string>? MergeQueryParameters(params IReadOnlyDictionary<string, string>?[]? multipleQueryParameters)
{
if (multipleQueryParameters == null || multipleQueryParameters.Length == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ public GetLengthsCriteria(int age, int increment)
LengthsAge = age;
LengthsIncr = increment;
}

public int LengthsAge { get; private set; }
public int LengthsIncr { get; private set; }

public IReadOnlyDictionary<string, string> ToQueryParameters()
{
return new Dictionary<string, string>
{
{nameof(LengthsAge), LengthsAge.ToString()},
{nameof(LengthsIncr), LengthsIncr.ToString()}
{ "lengths_age", LengthsAge.ToString() },
{ "lengths_incr", LengthsIncr.ToString() }
};
}
}
Loading