Skip to content

Commit

Permalink
Initial client code (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianhall authored May 1, 2024
1 parent af00695 commit 359f104
Show file tree
Hide file tree
Showing 72 changed files with 5,674 additions and 916 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@

<Import Project="..\Shared.Build.props" />

<PropertyGroup>
<Nullable>disable</Nullable>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="CommunityToolkit.Datasync.Client.Test" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Ensure.That" Version="10.1.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<Using Include="EnsureThat" />
<ProjectReference Include="..\CommunityToolkit.Datasync.Server.Abstractions\CommunityToolkit.Datasync.Server.Abstractions.csproj" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.Azure.Core.Spatial">
<HintPath>..\..\..\..\..\..\..\Nuget\microsoft.azure.core.spatial\1.1.0\lib\netstandard2.0\Microsoft.Azure.Core.Spatial.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

This file was deleted.

10 changes: 10 additions & 0 deletions src/CommunityToolkit.Datasync.Client/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("Style", "IDE0058:Expression value is never used",
Justification = "This is used in reflection and parameter checking.",
Scope = "namespaceanddescendants", Target = "~N:CommunityToolkit.Datasync.Client")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace CommunityToolkit.Datasync.Client.Http;

/// <summary>
/// Definition of an authentication provider, which is a specific type of
/// delegating handler that handles authentication updates.
/// </summary>
public abstract class AuthenticationProvider : DelegatingHandler
{
/// <summary>
/// The display name for the currently logged in user. This may be null.
/// </summary>
public string DisplayName { get; protected set; }

/// <summary>
/// If true, the user is logged in (and the UserId is available)
/// </summary>
public bool IsLoggedIn { get; protected set; }

/// <summary>
/// The user ID for this user.
/// </summary>
public string UserId { get; protected set; }

/// <summary>
/// Initiate a login request out of band of the pipeline. This can be used
/// to initiate the login process via a button.
/// </summary>
/// <returns>An async task that resolves when the login is complete</returns>
public abstract Task LoginAsync();
}
43 changes: 43 additions & 0 deletions src/CommunityToolkit.Datasync.Client/Http/AuthenticationToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace CommunityToolkit.Datasync.Client.Http;

/// <summary>
/// Definition of an authentication token response.
/// </summary>
public struct AuthenticationToken
{
/// <summary>
/// The display name for this user.
/// </summary>
public string DisplayName { get; set; }

/// <summary>
/// The expiry date of the JWT Token
/// </summary>
public DateTimeOffset ExpiresOn { get; set; }
/// <summary>
/// The actual JWT Token
/// </summary>
public string Token { get; set; }

/// <summary>
/// The User Id for this user
/// </summary>
public string UserId { get; set; }

/// <summary>
/// Return a visual representation of the authentication token for logging purposes.
/// </summary>
/// <returns>The string representation of the authentication token</returns>
public override readonly string ToString()
{
string displayName = DisplayName is null ? "null" : $"\"{DisplayName}\"";
string expiresOn = ExpiresOn.ToString("O", System.Globalization.CultureInfo.InvariantCulture);
string token = Token is null ? "null" : $"\"{Token}\"";
string userId = UserId is null ? "null" : $"\"{UserId}\"";
return $"AuthenticationToken(DisplayName={displayName},ExpiresOn=\"{expiresOn}\",Token={token},UserId={userId})";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,62 +8,71 @@
namespace CommunityToolkit.Datasync.Client.Http;

/// <summary>
/// An implementation of the <see cref="IHttpClientFactory"/> that provides a
/// suitable client for use with the Datasync client.
/// A simple implementation of the <see cref="IHttpClientFactory"/>.
/// </summary>
internal class DatasyncHttpClientFactory(IDatasyncHttpClientOptions options) : IHttpClientFactory
/// <param name="options">The options to use in creating a client.</param>
public class DatasyncHttpClientFactory(IDatasyncHttpOptions options) : IHttpClientFactory
{
/// <summary>
/// A factory method for creating the default <see cref="HttpClientHandler"/>
/// The cache of <see cref="HttpClient"/> objects.
/// </summary>
protected Func<HttpMessageHandler> DefaultHandlerFactory = GetDefaultHttpClientHandler;
private readonly ConcurrentDictionary<string, HttpClient> _clients = new();

/// <summary>
/// A cache for the <see cref="HttpClient" /> instances created by this factory.
/// A factory method for creating the default <see cref="HttpClientHandler"/>.
/// </summary>
protected readonly ConcurrentDictionary<string, HttpClient> _clients = new();
protected Func<HttpMessageHandler> DefaultHandlerFactory = GetDefaultHttpClientHandler;

/// <inheritdoc />
/// <summary>
/// Creates a new <see cref="HttpClient"/> based on the options provided.
/// </summary>
/// <param name="name">The name of the client to create.</param>
/// <returns>The created client.</returns>
public HttpClient CreateClient(string name)
{
Ensure.That(Options.BaseAddress).IsValidDatasyncUri();

if (this._clients.TryGetValue(name, out HttpClient? client))
name ??= "";
if (this._clients.TryGetValue(name, out HttpClient client))
{
return client;
}

HttpMessageHandler roothandler = CreatePipeline();
HttpClient newclient = new(roothandler, disposeHandler: true) { BaseAddress = Options.BaseAddress, Timeout = Options.HttpTimeout };
foreach (KeyValuePair<string, string> header in Options.HttpRequestHeaders)
HttpMessageHandler rootHandler = CreatePipeline(Options.HttpPipeline ?? []);
client = new HttpClient(rootHandler) { Timeout = Options.HttpTimeout };
if (Options.HttpRequestHeaders is not null)
{
newclient.DefaultRequestHeaders.Add(header.Key, header.Value);
foreach (KeyValuePair<string, string> header in Options.HttpRequestHeaders)
{
if (!client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value))
{
client.DefaultRequestHeaders.Add(header.Key, header.Value);
}
}
}

// Attempt to add the client to the cache. Don't worry if it doesn't work, as we'll just return the new client.
_ = this._clients.TryAdd(name, newclient);
return newclient;
// We don't really care if we fail to add the client since we'll just return the one we created
_ = this._clients.TryAdd(name, client);
return client;
}

/// <summary>
/// The options to use when creating the <see cref="HttpClient" /> instances.
/// </summary>
internal IDatasyncHttpClientOptions Options { get; } = options;
internal IDatasyncHttpOptions Options { get; set; } = options;

/// <summary>
/// Transform a list of <see cref="HttpMessageHandler"/> objects into a chain suitable for using as the pipeline of a <see cref="HttpClient"/>.
/// Transforms a list of <see cref="HttpMessageHandler"/> instances into a chain of handlers suitable for
/// using as a pipeline in an <see cref="HttpClient"/>.
/// </summary>
/// <returns>The chained <see cref="HttpMessageHandler"/></returns>
internal HttpMessageHandler CreatePipeline()
/// <param name="handlers">The ordered list of <see cref="HttpMessageHandler"/> objects to transform.</param>
/// <returns>The chained <see cref="HttpMessageHandler"/> objects.</returns>
/// <exception cref="ArgumentException">Thrown if the ordered list is invalid.</exception>
internal HttpMessageHandler CreatePipeline(IEnumerable<HttpMessageHandler> handlers)
{
HttpMessageHandler pipeline = Options.HttpPipeline.LastOrDefault() ?? this.DefaultHandlerFactory();
HttpMessageHandler pipeline = handlers.LastOrDefault() ?? this.DefaultHandlerFactory();
if (pipeline is DelegatingHandler lastPolicy && lastPolicy.InnerHandler is null)
{
lastPolicy.InnerHandler = this.DefaultHandlerFactory();
pipeline = lastPolicy;
}

foreach (HttpMessageHandler handler in Options.HttpPipeline.Reverse().Skip(1))
foreach (HttpMessageHandler handler in handlers.Reverse().Skip(1))
{
if (handler is DelegatingHandler policy)
{
Expand All @@ -72,17 +81,17 @@ internal HttpMessageHandler CreatePipeline()
}
else
{
throw new InvalidOperationException("All message handlers except the last one must be 'DelegatingHandler' instances. The last handler may be a 'HttpClientHandler' instance.");
throw new ArgumentException("All message handlers except the last one must be a 'DelegatingHandler'", nameof(handlers));
}
}

return pipeline;
}

/// <summary>
/// Returns a default <see cref="HttpClientHandler"/> instance that supports automatic decompression.
/// Returns a <see cref="HttpClientHandler"/> that support automatic decompression.
/// </summary>
/// <returns></returns>
/// <returns>A <see cref="HttpMessageHandler"/>.</returns>
protected static HttpMessageHandler GetDefaultHttpClientHandler()
{
HttpClientHandler handler = new();
Expand Down

This file was deleted.

Loading

0 comments on commit 359f104

Please sign in to comment.