From e370e4ba671feb530a5481b4595413c7fd032b9b Mon Sep 17 00:00:00 2001 From: David Pine Date: Mon, 6 Jan 2025 22:54:11 -0600 Subject: [PATCH] Added support for .NET Aspire and a client-lib. --- Directory.Packages.props | 1 + .../ProfanityFilter.Api/GlobalUsings.cs | 6 + .../ProfanityFilter.Api.csproj | 19 ++++ .../ProfanityFilter.Api.http | 6 + playground/ProfanityFilter.Api/Program.cs | 37 +++++++ .../appsettings.Development.json | 8 ++ .../ProfanityFilter.Api/appsettings.json | 9 ++ .../ProfanityFilter.AppHost.csproj | 1 + playground/ProfanityFilter.AppHost/Program.cs | 8 +- profanity-filter.sln | 21 ++++ .../DefaultRealtimeClient.cs | 87 +++++++++------ .../DefaultRestClient.cs | 10 +- .../ProfanityFilterClientExtensions.cs | 32 +++++- src/ProfanityFilter.Client/GlobalUsings.cs | 10 +- src/ProfanityFilter.Client/IRealtimeClient.cs | 7 +- .../Options/ProfanityFilterOptions.cs | 10 -- .../ProfanityFilter.Client.csproj | 10 +- src/ProfanityFilter.Hosting/GlobalUsings.cs | 2 - .../ProfanityFilterContainerImageDetails.cs | 4 +- ...rofanityFilterResourceBuilderExtensions.cs | 22 +++- src/ProfanityFilter.Services/GlobalUsings.cs | 7 -- .../ProfanityFilter.Services.csproj | 10 +- .../Extensions/ConfigurationExtensions.cs | 11 +- .../Extensions/MaybeExtensions.cs | 3 +- src/ProfanityFilter.Shared/GlobalUsings.cs | 2 - .../Optional/Nothing.cs | 17 ++- .../Optional/Something.cs | 17 ++- .../Components/Pages/Home.razor.cs | 104 +++--------------- src/ProfanityFilter.WebApi/GlobalUsings.cs | 7 +- .../ProfanityFilter.WebApi.csproj | 1 + src/ProfanityFilter.WebApi/Program.cs | 22 ++-- .../Properties/launchSettings.json | 35 ++++++ .../Services/BaseAddressResolver.cs | 25 ----- .../appsettings.Development.json | 3 + src/ProfanityFilter.WebApi/appsettings.json | 5 +- .../GlobalUsings.cs | 7 ++ .../ProfanityFilter.Client.Tests.csproj | 33 ++++++ .../ProfanityFilterClientTests.cs | 33 ++++++ .../TestRealTimeClient.cs | 22 ++++ .../TestRestClient.cs | 32 ++++++ .../GlobalUsings.cs | 7 ++ .../MaybeExtensionsTests.cs | 52 +++++++++ .../NothingTests.cs | 17 +++ .../ProfanityFilter.Shared.Tests.csproj | 33 ++++++ .../SomethingTests.cs | 33 ++++++ 45 files changed, 621 insertions(+), 227 deletions(-) create mode 100644 playground/ProfanityFilter.Api/GlobalUsings.cs create mode 100644 playground/ProfanityFilter.Api/ProfanityFilter.Api.csproj create mode 100644 playground/ProfanityFilter.Api/ProfanityFilter.Api.http create mode 100644 playground/ProfanityFilter.Api/Program.cs create mode 100644 playground/ProfanityFilter.Api/appsettings.Development.json create mode 100644 playground/ProfanityFilter.Api/appsettings.json create mode 100644 src/ProfanityFilter.WebApi/Properties/launchSettings.json delete mode 100644 src/ProfanityFilter.WebApi/Services/BaseAddressResolver.cs create mode 100644 tests/ProfanityFilter.Client.Tests/GlobalUsings.cs create mode 100644 tests/ProfanityFilter.Client.Tests/ProfanityFilter.Client.Tests.csproj create mode 100644 tests/ProfanityFilter.Client.Tests/ProfanityFilterClientTests.cs create mode 100644 tests/ProfanityFilter.Client.Tests/TestRealTimeClient.cs create mode 100644 tests/ProfanityFilter.Client.Tests/TestRestClient.cs create mode 100644 tests/ProfanityFilter.Shared.Tests/GlobalUsings.cs create mode 100644 tests/ProfanityFilter.Shared.Tests/MaybeExtensionsTests.cs create mode 100644 tests/ProfanityFilter.Shared.Tests/NothingTests.cs create mode 100644 tests/ProfanityFilter.Shared.Tests/ProfanityFilter.Shared.Tests.csproj create mode 100644 tests/ProfanityFilter.Shared.Tests/SomethingTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index d504b5a..e0b2223 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,6 +16,7 @@ + diff --git a/playground/ProfanityFilter.Api/GlobalUsings.cs b/playground/ProfanityFilter.Api/GlobalUsings.cs new file mode 100644 index 0000000..256f9f1 --- /dev/null +++ b/playground/ProfanityFilter.Api/GlobalUsings.cs @@ -0,0 +1,6 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +global using Microsoft.AspNetCore.Mvc; +global using ProfanityFilter.Client; +global using ProfanityFilter.Shared.Api; \ No newline at end of file diff --git a/playground/ProfanityFilter.Api/ProfanityFilter.Api.csproj b/playground/ProfanityFilter.Api/ProfanityFilter.Api.csproj new file mode 100644 index 0000000..42891c7 --- /dev/null +++ b/playground/ProfanityFilter.Api/ProfanityFilter.Api.csproj @@ -0,0 +1,19 @@ + + + + $(DefaultTargetFramework) + enable + enable + 82eaa4db-aed7-4828-8fec-bb9d542765f4 + + + + + + + + + + + + diff --git a/playground/ProfanityFilter.Api/ProfanityFilter.Api.http b/playground/ProfanityFilter.Api/ProfanityFilter.Api.http new file mode 100644 index 0000000..7f82f91 --- /dev/null +++ b/playground/ProfanityFilter.Api/ProfanityFilter.Api.http @@ -0,0 +1,6 @@ +@ProfanityFilter.Api_HostAddress = http://localhost:5225 + +GET {{ProfanityFilter.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/playground/ProfanityFilter.Api/Program.cs b/playground/ProfanityFilter.Api/Program.cs new file mode 100644 index 0000000..423b819 --- /dev/null +++ b/playground/ProfanityFilter.Api/Program.cs @@ -0,0 +1,37 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.AddProfanityFilterClient("profanity-filter"); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseSwagger(); +app.UseSwaggerUI(); +app.UseStaticFiles(); + +app.UseHttpsRedirection(); + +app.MapPost( + pattern: "/filter", + handler: static async ( + [FromBody] ProfanityFilterRequest request, + [FromServices] IRestClient client) => + await client.ApplyFilterAsync(request)) + .WithName("Filter"); + +app.Run(); diff --git a/playground/ProfanityFilter.Api/appsettings.Development.json b/playground/ProfanityFilter.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/playground/ProfanityFilter.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/ProfanityFilter.Api/appsettings.json b/playground/ProfanityFilter.Api/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/playground/ProfanityFilter.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/playground/ProfanityFilter.AppHost/ProfanityFilter.AppHost.csproj b/playground/ProfanityFilter.AppHost/ProfanityFilter.AppHost.csproj index c173f97..5368196 100644 --- a/playground/ProfanityFilter.AppHost/ProfanityFilter.AppHost.csproj +++ b/playground/ProfanityFilter.AppHost/ProfanityFilter.AppHost.csproj @@ -17,6 +17,7 @@ + diff --git a/playground/ProfanityFilter.AppHost/Program.cs b/playground/ProfanityFilter.AppHost/Program.cs index e7eb995..2530345 100644 --- a/playground/ProfanityFilter.AppHost/Program.cs +++ b/playground/ProfanityFilter.AppHost/Program.cs @@ -3,7 +3,11 @@ var builder = DistributedApplication.CreateBuilder(args); -_ = builder.AddProfanityFilter("profanity-filter") - .WithDataBindMount("./CustomData"); +var filter = builder.AddProfanityFilter("profanity-filter") + .WithCustomDataBindMount("./CustomData"); + +builder.AddProject("api") + .WithReference(filter) + .WaitFor(filter); builder.Build().Run(); diff --git a/profanity-filter.sln b/profanity-filter.sln index 007f8a4..f5dacf1 100644 --- a/profanity-filter.sln +++ b/profanity-filter.sln @@ -42,6 +42,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "playground", "playground", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProfanityFilter.AppHost", "playground\ProfanityFilter.AppHost\ProfanityFilter.AppHost.csproj", "{B5E3CB82-3306-4DD6-96BB-17995027FCA3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProfanityFilter.Api", "playground\ProfanityFilter.Api\ProfanityFilter.Api.csproj", "{5380A680-6C10-4EA5-92F2-FA7DBF78900C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProfanityFilter.Client.Tests", "tests\ProfanityFilter.Client.Tests\ProfanityFilter.Client.Tests.csproj", "{2DDAB9F2-3BC8-4191-B345-7DCA606BD46E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProfanityFilter.Shared.Tests", "tests\ProfanityFilter.Shared.Tests\ProfanityFilter.Shared.Tests.csproj", "{A742317F-FFB8-4A42-B9DC-8F79DA3B8EC6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -84,6 +90,18 @@ Global {B5E3CB82-3306-4DD6-96BB-17995027FCA3}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5E3CB82-3306-4DD6-96BB-17995027FCA3}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5E3CB82-3306-4DD6-96BB-17995027FCA3}.Release|Any CPU.Build.0 = Release|Any CPU + {5380A680-6C10-4EA5-92F2-FA7DBF78900C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5380A680-6C10-4EA5-92F2-FA7DBF78900C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5380A680-6C10-4EA5-92F2-FA7DBF78900C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5380A680-6C10-4EA5-92F2-FA7DBF78900C}.Release|Any CPU.Build.0 = Release|Any CPU + {2DDAB9F2-3BC8-4191-B345-7DCA606BD46E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DDAB9F2-3BC8-4191-B345-7DCA606BD46E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DDAB9F2-3BC8-4191-B345-7DCA606BD46E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DDAB9F2-3BC8-4191-B345-7DCA606BD46E}.Release|Any CPU.Build.0 = Release|Any CPU + {A742317F-FFB8-4A42-B9DC-8F79DA3B8EC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A742317F-FFB8-4A42-B9DC-8F79DA3B8EC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A742317F-FFB8-4A42-B9DC-8F79DA3B8EC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A742317F-FFB8-4A42-B9DC-8F79DA3B8EC6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -98,6 +116,9 @@ Global {482EDC8C-D8F1-4934-8274-F24D14D9E4B0} = {28623E11-D15F-4448-82F6-B86A7D59B1D4} {ED750950-FD39-4FF9-B3DC-48FC171164F7} = {28623E11-D15F-4448-82F6-B86A7D59B1D4} {B5E3CB82-3306-4DD6-96BB-17995027FCA3} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {5380A680-6C10-4EA5-92F2-FA7DBF78900C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {2DDAB9F2-3BC8-4191-B345-7DCA606BD46E} = {EAC63754-09D3-49C9-ACDF-ECF73AA1D922} + {A742317F-FFB8-4A42-B9DC-8F79DA3B8EC6} = {EAC63754-09D3-49C9-ACDF-ECF73AA1D922} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {72DBDAF8-D0CD-4A55-A944-0E1787FFA5C6} diff --git a/src/ProfanityFilter.Client/DefaultRealtimeClient.cs b/src/ProfanityFilter.Client/DefaultRealtimeClient.cs index 3a5ad60..1ea8e0c 100644 --- a/src/ProfanityFilter.Client/DefaultRealtimeClient.cs +++ b/src/ProfanityFilter.Client/DefaultRealtimeClient.cs @@ -4,18 +4,54 @@ namespace ProfanityFilter.Client; internal sealed class DefaultRealtimeClient( + IConfiguration configuration, IOptions options, ILogger logger) : IRealtimeClient { - private readonly HubConnection _connection = new HubConnectionBuilder() - .WithUrl(options.Value.RealtimeUri) - .WithAutomaticReconnect() - .WithStatefulReconnect() - .Build(); + private readonly Uri _hubUrl = new UriBuilder( + options.Value.ApiBaseAddress ?? throw new ArgumentException(""" + The API base address must be provided. + """)) + { + Path = "profanity/hub" + }.Uri; + + private HubConnection? _connection = null; + + [MemberNotNull(nameof(_connection))] + private void EnsureInitialized() + { + _connection ??= new HubConnectionBuilder() + .WithUrl(_hubUrl, options => + { + // Only apply these options when running in a container. + if (!configuration.IsRunningInContainer()) + { + return; + } + + options.UseDefaultCredentials = true; + options.HttpMessageHandlerFactory = handler => + { + if (handler is HttpClientHandler clientHandler) + { + clientHandler.ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + } + + return handler; + }; + }) + .WithAutomaticReconnect() + .WithStatefulReconnect() + .Build(); + } /// async ValueTask IRealtimeClient.StartAsync(CancellationToken cancellationToken) { + EnsureInitialized(); + _connection.Closed += OnHubConnectionClosedAsync; _connection.Reconnected += OnHubConnectionReconnectedAsync; _connection.Reconnecting += OnHubConnectionReconnectingAsync; @@ -26,6 +62,8 @@ async ValueTask IRealtimeClient.StartAsync(CancellationToken cancellationToken) /// async ValueTask IRealtimeClient.StopAsync(CancellationToken cancellationToken) { + EnsureInitialized(); + _connection.Closed -= OnHubConnectionClosedAsync; _connection.Reconnected -= OnHubConnectionReconnectedAsync; _connection.Reconnecting -= OnHubConnectionReconnectingAsync; @@ -55,34 +93,21 @@ private Task OnHubConnectionReconnectedAsync(string? arg) } /// - async IAsyncEnumerable IRealtimeClient.LiveStreamAsync( - IAsyncEnumerable liveRequests, - [EnumeratorCancellation] CancellationToken cancellationToken) + bool IRealtimeClient.IsConnected => _connection is { - var channel = Channel.CreateUnbounded(); + State: HubConnectionState.Connected + }; - _connection.On( - "live", response => channel.Writer.TryWrite(response)); - - var sendTask = Task.Run(async () => - { - await foreach (var request in liveRequests.WithCancellation(cancellationToken)) - { - await _connection.SendAsync("LiveStream", request, cancellationToken); - } - - channel.Writer.Complete(); - }, - cancellationToken); - - while (await channel.Reader.WaitToReadAsync(cancellationToken)) - { - while (channel.Reader.TryRead(out var response)) - { - yield return response; - } - } + /// + IAsyncEnumerable IRealtimeClient.StreamAsync( + IAsyncEnumerable liveRequests, + CancellationToken cancellationToken) + { + EnsureInitialized(); - await sendTask; + return _connection.StreamAsync( + methodName: "live", + arg1: liveRequests, + cancellationToken: cancellationToken); } } \ No newline at end of file diff --git a/src/ProfanityFilter.Client/DefaultRestClient.cs b/src/ProfanityFilter.Client/DefaultRestClient.cs index f1283e9..f64980a 100644 --- a/src/ProfanityFilter.Client/DefaultRestClient.cs +++ b/src/ProfanityFilter.Client/DefaultRestClient.cs @@ -10,7 +10,7 @@ internal sealed class DefaultRestClient(HttpClient client) : IRestClient async Task> IRestClient.ApplyFilterAsync(ProfanityFilterRequest request) { using var response = await client.PostAsJsonAsync( - "filter", request, JsonSerializationContext.Default.ProfanityFilterRequest); + "profanity/filter", request, JsonSerializationContext.Default.ProfanityFilterRequest); response.EnsureSuccessStatusCode(); @@ -23,25 +23,25 @@ async Task> IRestClient.ApplyFilterAsync(Profani /// Task> IRestClient.GetDataByNameAsync(string name) { - return GetMaybeAsync($"data/{name}", JsonSerializationContext.Default.StringArray); + return GetMaybeAsync($"profanity/data/{name}", JsonSerializationContext.Default.StringArray); } /// Task> IRestClient.GetDataNamesAsync() { - return GetMaybeAsync("data", JsonSerializationContext.Default.StringArray); + return GetMaybeAsync("profanity/data", JsonSerializationContext.Default.StringArray); } /// Task> IRestClient.GetStrategiesAsync() { - return GetMaybeAsync("strategies", JsonSerializationContext.Default.StrategyResponseArray); + return GetMaybeAsync("profanity/strategies", JsonSerializationContext.Default.StrategyResponseArray); } /// Task> IRestClient.GetTargetsAsync() { - return GetMaybeAsync("targets", JsonSerializationContext.Default.FilterTargetResponseArray); + return GetMaybeAsync("profanity/targets", JsonSerializationContext.Default.FilterTargetResponseArray); } /// diff --git a/src/ProfanityFilter.Client/Extensions/ProfanityFilterClientExtensions.cs b/src/ProfanityFilter.Client/Extensions/ProfanityFilterClientExtensions.cs index 81dde4c..2ac7daf 100644 --- a/src/ProfanityFilter.Client/Extensions/ProfanityFilterClientExtensions.cs +++ b/src/ProfanityFilter.Client/Extensions/ProfanityFilterClientExtensions.cs @@ -57,18 +57,40 @@ private static void AddProfanityFilterClient( configSection.Bind(options); namedConfigSection.Bind(options); - if (builder.Configuration.GetConnectionString(connectionName) is string connectionString && - Uri.TryCreate(connectionString, UriKind.Absolute, out var baseAddress)) + builder.Services.AddOptions() + .Bind(configSection) + .Bind(namedConfigSection); + + //builder.Services.Configure(configSection); + //builder.Services.Configure(namedConfigSection); + + var connectionString = builder.Configuration.GetConnectionString(connectionName) + ?? configSection["ApiBaseAddress"] + ?? namedConfigSection["ApiBaseAddress"]; + + if (connectionString is string potentialUri && + Uri.TryCreate(potentialUri, UriKind.Absolute, out var baseAddress)) { + builder.Services.Configure( + configureOptions: o => o.ApiBaseAddress = baseAddress); + options.ApiBaseAddress = baseAddress; } - configure?.Invoke(options); + if (configure is not null) + { + configure.Invoke(options); + builder.Services.PostConfigure(configure); + } - builder.Services.AddOptions(); builder.Services.AddLogging(); + builder.Services.AddHttpClient((sp, client) => + { + var options = sp.GetRequiredService>(); + + client.BaseAddress = options.Value.ApiBaseAddress; + }); - builder.Services.AddScoped(); builder.Services.AddScoped(); if (serviceKey is null) diff --git a/src/ProfanityFilter.Client/GlobalUsings.cs b/src/ProfanityFilter.Client/GlobalUsings.cs index 87c076b..3d45226 100644 --- a/src/ProfanityFilter.Client/GlobalUsings.cs +++ b/src/ProfanityFilter.Client/GlobalUsings.cs @@ -1,21 +1,15 @@ // Copyright (c) David Pine. All rights reserved. // Licensed under the MIT License. -global using System; -global using System.Collections.Frozen; -global using System.ComponentModel.DataAnnotations; global using System.Diagnostics.CodeAnalysis; global using System.Net.Http.Json; -global using System.Runtime.CompilerServices; -global using System.Text.Json; -global using System.Text.Json.Serialization; global using System.Text.Json.Serialization.Metadata; -global using System.Threading.Channels; global using Microsoft.AspNetCore.SignalR.Client; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; -global using Microsoft.Extensions.Hosting; global using ProfanityFilter.Client; global using ProfanityFilter.Client.Options; diff --git a/src/ProfanityFilter.Client/IRealtimeClient.cs b/src/ProfanityFilter.Client/IRealtimeClient.cs index d3d18ce..a62a495 100644 --- a/src/ProfanityFilter.Client/IRealtimeClient.cs +++ b/src/ProfanityFilter.Client/IRealtimeClient.cs @@ -8,6 +8,11 @@ namespace ProfanityFilter.Client; /// public interface IRealtimeClient { + /// + /// Gets a value indicating whether the client is connected. + /// + bool IsConnected { get; } + /// /// Starts the real-time client asynchronously. /// @@ -28,7 +33,7 @@ public interface IRealtimeClient /// An asynchronous stream of profanity filter requests. /// A token to cancel the operation. /// An asynchronous stream of profanity filter responses. - IAsyncEnumerable LiveStreamAsync( + IAsyncEnumerable StreamAsync( IAsyncEnumerable liveRequests, CancellationToken cancellationToken); } diff --git a/src/ProfanityFilter.Client/Options/ProfanityFilterOptions.cs b/src/ProfanityFilter.Client/Options/ProfanityFilterOptions.cs index 33cf4c8..b9f6ea9 100644 --- a/src/ProfanityFilter.Client/Options/ProfanityFilterOptions.cs +++ b/src/ProfanityFilter.Client/Options/ProfanityFilterOptions.cs @@ -23,16 +23,6 @@ public sealed class ProfanityFilterOptions /// public Uri? ApiBaseAddress { get; set; } - /// - /// Gets the URI for the profanity REST API. - /// - public string RestUri => $"{ApiBaseAddress}/profanity"; - - /// - /// Gets the URI for the real-time profanity hub. - /// - public string RealtimeUri => $"{RestUri}/hub"; - /// /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not. /// diff --git a/src/ProfanityFilter.Client/ProfanityFilter.Client.csproj b/src/ProfanityFilter.Client/ProfanityFilter.Client.csproj index f1c457d..80f0e47 100644 --- a/src/ProfanityFilter.Client/ProfanityFilter.Client.csproj +++ b/src/ProfanityFilter.Client/ProfanityFilter.Client.csproj @@ -6,18 +6,24 @@ enable - + + + + + + + diff --git a/src/ProfanityFilter.Hosting/GlobalUsings.cs b/src/ProfanityFilter.Hosting/GlobalUsings.cs index fb9a6b7..b94e354 100644 --- a/src/ProfanityFilter.Hosting/GlobalUsings.cs +++ b/src/ProfanityFilter.Hosting/GlobalUsings.cs @@ -2,8 +2,6 @@ // Licensed under the MIT License. global using System.Diagnostics; - -global using Aspire.Hosting; global using Aspire.Hosting.ApplicationModel; global using Microsoft.Extensions.DependencyInjection; diff --git a/src/ProfanityFilter.Hosting/ProfanityFilterContainerImageDetails.cs b/src/ProfanityFilter.Hosting/ProfanityFilterContainerImageDetails.cs index 2ef2480..4ead3ae 100644 --- a/src/ProfanityFilter.Hosting/ProfanityFilterContainerImageDetails.cs +++ b/src/ProfanityFilter.Hosting/ProfanityFilterContainerImageDetails.cs @@ -17,6 +17,6 @@ internal static class ProfanityFilterContainerImageDetails /// ievangelist/profanity-filter-api internal const string Image = "ievangelist/profanity-filter-api"; - /// 2.0.9 - internal const string Tag = "2.0.9"; + /// 2.0.10 + internal const string Tag = "2.0.10"; } diff --git a/src/ProfanityFilter.Hosting/ProfanityFilterResourceBuilderExtensions.cs b/src/ProfanityFilter.Hosting/ProfanityFilterResourceBuilderExtensions.cs index 17d726f..6dfd7cf 100644 --- a/src/ProfanityFilter.Hosting/ProfanityFilterResourceBuilderExtensions.cs +++ b/src/ProfanityFilter.Hosting/ProfanityFilterResourceBuilderExtensions.cs @@ -19,6 +19,20 @@ public static class ProfanityFilterResourceBuilderExtensions /// The name of the resource. /// The optional password resource builder. /// An for the profanity filter resource. + /// + /// Add a profanity filter container to the application model and reference it in a .NET project. Additionally, in this + /// example a bind mount is added to the container to allow data to be persisted across container restarts. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// var profanityFilter = builder.AddProfanityFilter("profanity-filter"); + /// + /// var api = builder.AddProject<Projects.Api>("api") + /// .WithReference(profanityFilter); + /// + /// builder.Build().Run(); + /// + /// public static IResourceBuilder AddProfanityFilter( this IDistributedApplicationBuilder builder, [ResourceName] string name, @@ -41,7 +55,7 @@ public static IResourceBuilder AddProfanityFilter( } /// - /// Adds a bind mount for the data folder to a . + /// Adds a bind mount for the /app/CustomData folder in the . /// Added data files should be newline delimited and have a *.txt file extension. /// /// The resource builder. @@ -53,8 +67,8 @@ public static IResourceBuilder AddProfanityFilter( /// /// var builder = DistributedApplication.CreateBuilder(args); /// - /// var profanityFilter = builder.AddElasticsearch("profanity-filter") - /// .WithDataBindMount("./data"); + /// var profanityFilter = builder.AddProfanityFilter("profanity-filter") + /// .WithDataBindMount("./CustomData"); /// /// var api = builder.AddProject<Projects.Api>("api") /// .WithReference(profanityFilter); @@ -62,7 +76,7 @@ public static IResourceBuilder AddProfanityFilter( /// builder.Build().Run(); /// /// - public static IResourceBuilder WithDataBindMount( + public static IResourceBuilder WithCustomDataBindMount( this IResourceBuilder builder, string source) { diff --git a/src/ProfanityFilter.Services/GlobalUsings.cs b/src/ProfanityFilter.Services/GlobalUsings.cs index 1b5342b..6430fe5 100644 --- a/src/ProfanityFilter.Services/GlobalUsings.cs +++ b/src/ProfanityFilter.Services/GlobalUsings.cs @@ -3,15 +3,11 @@ global using System.Collections.Concurrent; global using System.Collections.Frozen; -global using System.Diagnostics.CodeAnalysis; global using System.Text; -global using System.Text.Json; -global using System.Text.Json.Serialization; global using System.Text.RegularExpressions; global using Microsoft.Extensions.Caching.Memory; global using Microsoft.Extensions.Logging; -global using Microsoft.Extensions.Configuration; global using Pathological.Globbing.Extensions; global using Pathological.Globbing.Options; @@ -22,9 +18,6 @@ global using ProfanityFilter.Services.Filters; global using ProfanityFilter.Services.Internals; global using ProfanityFilter.Shared; -global using ProfanityFilter.Shared.Api; -global using ProfanityFilter.Shared.Extensions; -global using ProfanityFilter.Shared.Optional; [assembly: System.Runtime.CompilerServices.InternalsVisibleTo( assemblyName: "ProfanityFilter.Services.Tests")] diff --git a/src/ProfanityFilter.Services/ProfanityFilter.Services.csproj b/src/ProfanityFilter.Services/ProfanityFilter.Services.csproj index 207c665..c983b28 100644 --- a/src/ProfanityFilter.Services/ProfanityFilter.Services.csproj +++ b/src/ProfanityFilter.Services/ProfanityFilter.Services.csproj @@ -7,12 +7,6 @@ $(NoWarn);NU5104 - - - Shared\%(RecursiveDir)%(Filename)%(Extension) - - - Always @@ -51,4 +45,8 @@ + + + + diff --git a/src/ProfanityFilter.Shared/Extensions/ConfigurationExtensions.cs b/src/ProfanityFilter.Shared/Extensions/ConfigurationExtensions.cs index 700bc25..9c723fc 100644 --- a/src/ProfanityFilter.Shared/Extensions/ConfigurationExtensions.cs +++ b/src/ProfanityFilter.Shared/Extensions/ConfigurationExtensions.cs @@ -7,8 +7,17 @@ namespace Microsoft.Extensions.Configuration; public static class ConfigurationExtensions { + /// + /// Determines whether the application is running inside a container. + /// + /// The configuration instance to check. + /// + /// true if the application is running inside a container; otherwise, false. + /// public static bool IsRunningInContainer(this IConfiguration configuration) { - return configuration.GetValue("DOTNET_RUNNING_IN_CONTAINER"); + return configuration.GetValue("DOTNET_RUNNING_IN_CONTAINER") + || Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") + is "true" or "TRUE" or "True" or "1"; } } diff --git a/src/ProfanityFilter.Shared/Extensions/MaybeExtensions.cs b/src/ProfanityFilter.Shared/Extensions/MaybeExtensions.cs index 7a1d7aa..15c2aad 100644 --- a/src/ProfanityFilter.Shared/Extensions/MaybeExtensions.cs +++ b/src/ProfanityFilter.Shared/Extensions/MaybeExtensions.cs @@ -27,7 +27,8 @@ public static IMaybe AsMaybe(this T? value) => value is null /// The function to transform the input value to the output value. /// An instance of containing the transformed value. public static IMaybe Chain( - this TIn @this, Func func) => + this IMaybe @this, + Func func) => @this switch { Something s => new Something(func.Invoke(s.Value)), diff --git a/src/ProfanityFilter.Shared/GlobalUsings.cs b/src/ProfanityFilter.Shared/GlobalUsings.cs index 75292e7..269a32f 100644 --- a/src/ProfanityFilter.Shared/GlobalUsings.cs +++ b/src/ProfanityFilter.Shared/GlobalUsings.cs @@ -6,7 +6,5 @@ global using System.Text.Json; global using System.Text.Json.Serialization; -global using Microsoft.Extensions.Configuration; - global using ProfanityFilter.Shared.Api; global using ProfanityFilter.Shared.Optional; \ No newline at end of file diff --git a/src/ProfanityFilter.Shared/Optional/Nothing.cs b/src/ProfanityFilter.Shared/Optional/Nothing.cs index beb2b4d..5533d4a 100644 --- a/src/ProfanityFilter.Shared/Optional/Nothing.cs +++ b/src/ProfanityFilter.Shared/Optional/Nothing.cs @@ -7,4 +7,19 @@ namespace ProfanityFilter.Shared.Optional; /// Represents a type that signifies the absence of a value. /// /// The type of the value that is absent. -public readonly record struct Nothing : IMaybe; +public readonly record struct Nothing : IMaybe +{ + /// + /// Implicitly converts the to its contained value of type . + /// + /// The instance to convert. + /// The value contained in the . + public static implicit operator T?(Nothing nothing) => default; + + /// + /// Implicitly converts the to a new . + /// + /// The value. + /// The new instance of . + public static implicit operator Nothing(T _) => new(); +} diff --git a/src/ProfanityFilter.Shared/Optional/Something.cs b/src/ProfanityFilter.Shared/Optional/Something.cs index 21e829a..af0ccd9 100644 --- a/src/ProfanityFilter.Shared/Optional/Something.cs +++ b/src/ProfanityFilter.Shared/Optional/Something.cs @@ -8,4 +8,19 @@ namespace ProfanityFilter.Shared.Optional; /// /// The type of the value. /// The value contained in the . -public readonly record struct Something(T Value) : IMaybe; +public readonly record struct Something(T Value) : IMaybe +{ + /// + /// Implicitly converts the to its contained value of type . + /// + /// The instance to convert. + /// The value contained in the . + public static implicit operator T(Something something) => something.Value; + + /// + /// Implicitly converts the to a new . + /// + /// The value. + /// The new instance of . + public static implicit operator Something(T value) => new(value); +} diff --git a/src/ProfanityFilter.WebApi/Components/Pages/Home.razor.cs b/src/ProfanityFilter.WebApi/Components/Pages/Home.razor.cs index 3404982..5857793 100644 --- a/src/ProfanityFilter.WebApi/Components/Pages/Home.razor.cs +++ b/src/ProfanityFilter.WebApi/Components/Pages/Home.razor.cs @@ -1,4 +1,7 @@ -namespace ProfanityFilter.WebApi.Components.Pages; +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace ProfanityFilter.WebApi.Components.Pages; [StreamRendering] public sealed partial class Home : IAsyncDisposable @@ -13,7 +16,6 @@ public sealed partial class Home : IAsyncDisposable private readonly ReplacementStrategy[] _strategies = Enum.GetValues(); private readonly CancellationTokenSource _cts = new(); - private HubConnection? _hub; private ReplacementStrategy _selectedStrategy; private string? _text = "Content to filter..."; private bool _isLoading = false; @@ -23,7 +25,7 @@ public sealed partial class Home : IAsyncDisposable public required ILogger Logger { get; set; } [Inject] - public required BaseAddressResolver BaseAddressResolver { get; set; } + public required IRealtimeClient RealtimeClient { get; set; } [Inject] public required ILocalStorageService LocalStorage { get; set; } @@ -59,91 +61,17 @@ protected override async Task OnAfterRenderAsync(bool firstRender) _selectedStrategy = selectedStrategy; StateHasChanged(); } + + await RealtimeClient.StartAsync(); - if (_hub is null) - { - var baseAddress = BaseAddressResolver.GetBaseAddress(); - - Logger.LogInformation("Connecting to: {Address}/profanity/hub", baseAddress); - - var uri = new UriBuilder(baseAddress) - { - Path = "/profanity/hub" - }; - - _hub = new HubConnectionBuilder() - .WithUrl(uri.Uri, options => - { - if (!BaseAddressResolver.IsRunningInContainer) - { - return; - } - - options.UseDefaultCredentials = true; - options.HttpMessageHandlerFactory = handler => - { - if (handler is HttpClientHandler clientHandler) - { - clientHandler.ServerCertificateCustomValidationCallback = - HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; - } - - return handler; - }; - }) - .WithAutomaticReconnect() - .WithStatefulReconnect() - .Build(); - - _hub.Closed += OnHubConnectionClosedAsync; - _hub.Reconnected += OnHubConnectionReconnectedAsync; - _hub.Reconnecting += OnHubConnectionReconnectingAsync; - - await _hub.StartAsync(); - - Logger.HubStarted(); - - _ = StartLiveUpdateStreamAsync(); - } - } - - private Task OnHubConnectionClosedAsync(Exception? exception) - { - Logger.HubConnectionClosed(exception); - - return Task.CompletedTask; - } - - private Task OnHubConnectionReconnectingAsync(Exception? exception) - { - Logger.HubReconnecting(exception); - - return Task.CompletedTask; - } - - private Task OnHubConnectionReconnectedAsync(string? arg) - { - Logger.HubReconnected(arg); + Logger.HubStarted(); - return Task.CompletedTask; - } - - [MemberNotNullWhen(false, nameof(_hub))] - private bool IsHubNotConnected() - { - if (_hub is null or not { State: HubConnectionState.Connected }) - { - Logger.NotConnectedToHub(); - - return true; - } - - return false; + _ = StartLiveUpdateStreamAsync(); } private async Task StartLiveUpdateStreamAsync() { - if (IsHubNotConnected() || _liveRequests is null) + if (!RealtimeClient.IsConnected || _liveRequests is null) { return; } @@ -156,8 +84,7 @@ private async Task StartLiveUpdateStreamAsync() var liveRequests = _liveRequests.ToAsyncEnumerable(); // Consumer responses... - await foreach (var response in _hub.StreamAsync( - methodName: "live", arg1: liveRequests, cancellationToken: _cts.Token)) + await foreach (var response in RealtimeClient.StreamAsync(liveRequests, cancellationToken: _cts.Token)) { if (response is null) { @@ -215,14 +142,9 @@ public async ValueTask DisposeAsync() await _cts.CancelAsync(); _cts.Dispose(); - if (_hub is not null) + if (RealtimeClient is not null) { - _hub.Closed -= OnHubConnectionClosedAsync; - _hub.Reconnected -= OnHubConnectionReconnectedAsync; - _hub.Reconnecting -= OnHubConnectionReconnectingAsync; - - await _hub.StopAsync(); - await _hub.DisposeAsync(); + await RealtimeClient.StopAsync(); } } } diff --git a/src/ProfanityFilter.WebApi/GlobalUsings.cs b/src/ProfanityFilter.WebApi/GlobalUsings.cs index 62d5009..06fd4e5 100644 --- a/src/ProfanityFilter.WebApi/GlobalUsings.cs +++ b/src/ProfanityFilter.WebApi/GlobalUsings.cs @@ -2,9 +2,9 @@ // Licensed under the MIT License. global using System.Diagnostics; -global using System.Diagnostics.CodeAnalysis; global using System.Reactive.Linq; global using System.Runtime.CompilerServices; +global using System.Text.Json; global using System.Timers; global using Markdig; @@ -12,20 +12,18 @@ global using Microsoft.AspNetCore.Components; global using Microsoft.AspNetCore.DataProtection; global using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; -global using Microsoft.AspNetCore.Hosting.Server; -global using Microsoft.AspNetCore.Hosting.Server.Features; global using Microsoft.AspNetCore.Http.Connections; global using Microsoft.AspNetCore.Http.HttpResults; global using Microsoft.AspNetCore.HttpLogging; global using Microsoft.AspNetCore.Mvc; global using Microsoft.AspNetCore.Mvc.ModelBinding; global using Microsoft.AspNetCore.SignalR; -global using Microsoft.AspNetCore.SignalR.Client; global using Microsoft.Extensions.Caching.Memory; global using Microsoft.Extensions.Compliance.Classification; global using Microsoft.Extensions.Compliance.Redaction; global using Microsoft.JSInterop; +global using ProfanityFilter.Client; global using ProfanityFilter.Services; global using ProfanityFilter.Shared; global using ProfanityFilter.Shared.Api; @@ -33,6 +31,5 @@ global using ProfanityFilter.WebApi.Components; global using ProfanityFilter.WebApi.Endpoints; global using ProfanityFilter.WebApi.Hubs; -global using ProfanityFilter.WebApi.Services; global using SystemTimer = System.Timers.Timer; \ No newline at end of file diff --git a/src/ProfanityFilter.WebApi/ProfanityFilter.WebApi.csproj b/src/ProfanityFilter.WebApi/ProfanityFilter.WebApi.csproj index 79485a2..b4e8d2f 100644 --- a/src/ProfanityFilter.WebApi/ProfanityFilter.WebApi.csproj +++ b/src/ProfanityFilter.WebApi/ProfanityFilter.WebApi.csproj @@ -38,6 +38,7 @@ + diff --git a/src/ProfanityFilter.WebApi/Program.cs b/src/ProfanityFilter.WebApi/Program.cs index 84fdb8a..e54c1a2 100644 --- a/src/ProfanityFilter.WebApi/Program.cs +++ b/src/ProfanityFilter.WebApi/Program.cs @@ -3,14 +3,7 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services.AddScoped(sp => -{ - var server = sp.GetRequiredService(); - - return server.Features.Get() - ?? throw new InvalidOperationException("There's no IServerAddressesFeature in the IServer."); -}); -builder.Services.AddScoped(); +builder.AddProfanityFilterClient("profanity-filter"); builder.Services.AddRedaction(static redaction => redaction.SetRedactor( @@ -24,8 +17,7 @@ builder.Services.AddLocalStorageServices(); builder.Services.AddMemoryCache(); -builder.Services.AddSignalR( - static options => options.EnableDetailedErrors = true); +builder.Services.AddSignalR(static options => options.EnableDetailedErrors = true); builder.Services.AddAntiforgery(); builder.Services.AddDataProtection() @@ -42,10 +34,7 @@ .AddInteractiveServerComponents(); builder.Services.ConfigureHttpJsonOptions( - static options => - options.SerializerOptions.TypeInfoResolverChain.Insert( - 0, - JsonSerializationContext.Default)); + static options => AssignJsonSerializerContext(options.SerializerOptions)); var app = builder.Build(); @@ -60,3 +49,8 @@ .AddInteractiveServerRenderMode(); app.Run(); + +static void AssignJsonSerializerContext(JsonSerializerOptions options) +{ + options.TypeInfoResolverChain.Insert(0, JsonSerializationContext.Default); +} diff --git a/src/ProfanityFilter.WebApi/Properties/launchSettings.json b/src/ProfanityFilter.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..b394beb --- /dev/null +++ b/src/ProfanityFilter.WebApi/Properties/launchSettings.json @@ -0,0 +1,35 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5276", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7097;http://localhost:5276", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (.NET SDK)": { + "commandName": "SdkContainer", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + } +} \ No newline at end of file diff --git a/src/ProfanityFilter.WebApi/Services/BaseAddressResolver.cs b/src/ProfanityFilter.WebApi/Services/BaseAddressResolver.cs deleted file mode 100644 index fcce824..0000000 --- a/src/ProfanityFilter.WebApi/Services/BaseAddressResolver.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) David Pine. All rights reserved. -// Licensed under the MIT License. - -namespace ProfanityFilter.WebApi.Services; - -public sealed class BaseAddressResolver( - IConfiguration configuration, - IServerAddressesFeature serverAddresses, - NavigationManager navigationManager) -{ - public bool IsRunningInContainer => configuration.IsRunningInContainer(); - - public string GetBaseAddress() - { - return IsRunningInContainer switch - { - // When running in a container, use the internal container port. - true => "https://localhost:8081", - - // Otherwise, rely on the server address or the base URI. - _ => serverAddresses.Addresses.FirstOrDefault(address => address.StartsWith("https")) - ?? navigationManager.BaseUri - }; - } -} diff --git a/src/ProfanityFilter.WebApi/appsettings.Development.json b/src/ProfanityFilter.WebApi/appsettings.Development.json index 770d3e9..83f446a 100644 --- a/src/ProfanityFilter.WebApi/appsettings.Development.json +++ b/src/ProfanityFilter.WebApi/appsettings.Development.json @@ -5,5 +5,8 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "ProfanityFilter": { + "ApiBaseAddress": "https://localhost:7097" } } diff --git a/src/ProfanityFilter.WebApi/appsettings.json b/src/ProfanityFilter.WebApi/appsettings.json index c5f5d9e..55d51a9 100644 --- a/src/ProfanityFilter.WebApi/appsettings.json +++ b/src/ProfanityFilter.WebApi/appsettings.json @@ -6,5 +6,8 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ProfanityFilter": { + "ApiBaseAddress": "https://localhost:8081" + } } diff --git a/tests/ProfanityFilter.Client.Tests/GlobalUsings.cs b/tests/ProfanityFilter.Client.Tests/GlobalUsings.cs new file mode 100644 index 0000000..cd3d266 --- /dev/null +++ b/tests/ProfanityFilter.Client.Tests/GlobalUsings.cs @@ -0,0 +1,7 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +global using Microsoft.VisualStudio.TestTools.UnitTesting; + +global using ProfanityFilter.Shared.Api; +global using ProfanityFilter.Shared.Optional; \ No newline at end of file diff --git a/tests/ProfanityFilter.Client.Tests/ProfanityFilter.Client.Tests.csproj b/tests/ProfanityFilter.Client.Tests/ProfanityFilter.Client.Tests.csproj new file mode 100644 index 0000000..a3a0754 --- /dev/null +++ b/tests/ProfanityFilter.Client.Tests/ProfanityFilter.Client.Tests.csproj @@ -0,0 +1,33 @@ + + + + $(DefaultTargetFramework) + false + true + exe + true + linux-x64 + + true + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/ProfanityFilter.Client.Tests/ProfanityFilterClientTests.cs b/tests/ProfanityFilter.Client.Tests/ProfanityFilterClientTests.cs new file mode 100644 index 0000000..3c05bdd --- /dev/null +++ b/tests/ProfanityFilter.Client.Tests/ProfanityFilterClientTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace ProfanityFilter.Client.Tests; + +[TestClass] +public class ProfanityFilterClientTests +{ + [TestMethod] + public void DeconstructCorrectlyMaterializesNullClients() + { + var sut = new ProfanityFilterClient(null!, null!); + + var (rest, realtime) = sut; + + Assert.IsNull(rest); + Assert.IsNull(realtime); + } + + [TestMethod] + public void DeconstructCorrectlyMaterializesTargetClients() + { + var sut = new ProfanityFilterClient( + new TestRestClient(), new TestRealTimeClient()); + + var (rest, realtime) = sut; + + Assert.IsNotNull(rest); + Assert.IsInstanceOfType(rest); + Assert.IsNotNull(realtime); + Assert.IsInstanceOfType(realtime); + } +} diff --git a/tests/ProfanityFilter.Client.Tests/TestRealTimeClient.cs b/tests/ProfanityFilter.Client.Tests/TestRealTimeClient.cs new file mode 100644 index 0000000..048a3af --- /dev/null +++ b/tests/ProfanityFilter.Client.Tests/TestRealTimeClient.cs @@ -0,0 +1,22 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace ProfanityFilter.Client.Tests; + +internal class TestRealTimeClient : IRealtimeClient +{ + IAsyncEnumerable IRealtimeClient.StreamAsync(IAsyncEnumerable liveRequests, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + ValueTask IRealtimeClient.StartAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + ValueTask IRealtimeClient.StopAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} diff --git a/tests/ProfanityFilter.Client.Tests/TestRestClient.cs b/tests/ProfanityFilter.Client.Tests/TestRestClient.cs new file mode 100644 index 0000000..dc30280 --- /dev/null +++ b/tests/ProfanityFilter.Client.Tests/TestRestClient.cs @@ -0,0 +1,32 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace ProfanityFilter.Client.Tests; + +internal class TestRestClient : IRestClient +{ + Task> IRestClient.ApplyFilterAsync(ProfanityFilterRequest request) + { + throw new NotImplementedException(); + } + + Task> IRestClient.GetDataByNameAsync(string name) + { + throw new NotImplementedException(); + } + + Task> IRestClient.GetDataNamesAsync() + { + throw new NotImplementedException(); + } + + Task> IRestClient.GetStrategiesAsync() + { + throw new NotImplementedException(); + } + + Task> IRestClient.GetTargetsAsync() + { + throw new NotImplementedException(); + } +} diff --git a/tests/ProfanityFilter.Shared.Tests/GlobalUsings.cs b/tests/ProfanityFilter.Shared.Tests/GlobalUsings.cs new file mode 100644 index 0000000..38634bf --- /dev/null +++ b/tests/ProfanityFilter.Shared.Tests/GlobalUsings.cs @@ -0,0 +1,7 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +global using Microsoft.VisualStudio.TestTools.UnitTesting; + +global using ProfanityFilter.Shared.Extensions; +global using ProfanityFilter.Shared.Optional; \ No newline at end of file diff --git a/tests/ProfanityFilter.Shared.Tests/MaybeExtensionsTests.cs b/tests/ProfanityFilter.Shared.Tests/MaybeExtensionsTests.cs new file mode 100644 index 0000000..ae04ab9 --- /dev/null +++ b/tests/ProfanityFilter.Shared.Tests/MaybeExtensionsTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace ProfanityFilter.Shared.Tests; + +[TestClass] +public class MaybeExtensionsTests +{ + [TestMethod] + public void AsMaybeWithNonNullValueReturnsSomething() + { + var value = "test"; + var result = value.AsMaybe(); + + Assert.IsInstanceOfType>(result); + Assert.AreEqual(value, ((Something)result).Value); + } + + [TestMethod] + public void AsMaybeWithNullValueReturnsNothing() + { + string? value = null; + var result = value.AsMaybe(); + + Assert.IsInstanceOfType>(result); + } + + [TestMethod] + public void ChainWithSomethingReturnsTransformedSomething() + { + var initial = new Something(5); + var result = initial.Chain(x => x * 2).Chain(x => x * 3.1); + + Assert.IsInstanceOfType>(result); + Assert.AreEqual(31d, ((Something)result).Value); + + var stringSomething = result.Chain(x => (x / 2).ToString()); + + Assert.IsInstanceOfType>(stringSomething); + Assert.AreEqual("15.5", ((Something)stringSomething).Value); + } + + [TestMethod] + public void ChainWithNothingReturnsNothing() + { + var initial = new Nothing(); + var result = initial.Chain(x => x * 2); + + Assert.IsInstanceOfType>(result); + } +} + diff --git a/tests/ProfanityFilter.Shared.Tests/NothingTests.cs b/tests/ProfanityFilter.Shared.Tests/NothingTests.cs new file mode 100644 index 0000000..3611a56 --- /dev/null +++ b/tests/ProfanityFilter.Shared.Tests/NothingTests.cs @@ -0,0 +1,17 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace ProfanityFilter.Shared.Tests; + +[TestClass] +public sealed class NothingTests +{ + [TestMethod] + public void NothingIsRepresentedAsSuch() + { + var sut = new Nothing(); + + Assert.IsInstanceOfType>(sut); + Assert.IsNotInstanceOfType(sut); + } +} diff --git a/tests/ProfanityFilter.Shared.Tests/ProfanityFilter.Shared.Tests.csproj b/tests/ProfanityFilter.Shared.Tests/ProfanityFilter.Shared.Tests.csproj new file mode 100644 index 0000000..862f4c3 --- /dev/null +++ b/tests/ProfanityFilter.Shared.Tests/ProfanityFilter.Shared.Tests.csproj @@ -0,0 +1,33 @@ + + + + $(DefaultTargetFramework) + false + true + exe + true + linux-x64 + + true + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/ProfanityFilter.Shared.Tests/SomethingTests.cs b/tests/ProfanityFilter.Shared.Tests/SomethingTests.cs new file mode 100644 index 0000000..3eca075 --- /dev/null +++ b/tests/ProfanityFilter.Shared.Tests/SomethingTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace ProfanityFilter.Shared.Tests; + +[TestClass] +public sealed class SomethingTests +{ + [TestMethod] + public void SomethingShouldContainValue() + { + // Arrange + var expectedValue = "test value"; + var something = new Something(expectedValue); + + // Act + var actualValue = something.Value; + + // Assert + Assert.AreEqual(expectedValue, actualValue); + } + + [TestMethod] + public void SomethingShouldBeAssignableToIMaybe() + { + // Arrange + var something = new Something(42); + + // Act & Assert + Assert.IsInstanceOfType>(something); + Assert.AreEqual(42, something.Value); + } +}