diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1096307..9c91dff 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,14 +7,14 @@
-
-
+
+
+
-
@@ -22,5 +22,6 @@
+
\ No newline at end of file
diff --git a/global.json b/global.json
index e2138e0..7761349 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,5 @@
{
"sdk": {
- "version": "8.0.200"
+ "version": "8.0.301"
}
}
\ No newline at end of file
diff --git a/profanity-filter.sln b/profanity-filter.sln
index 455ee08..b2c742a 100644
--- a/profanity-filter.sln
+++ b/profanity-filter.sln
@@ -1,4 +1,3 @@
-
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34004.107
@@ -29,12 +28,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{28623E11-D15
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{EAC63754-09D3-49C9-ACDF-ECF73AA1D922}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProfanityFilter.WebApi", "src\ProfanityFilter.WebApi\ProfanityFilter.WebApi.csproj", "{163A89FA-5F97-4332-B5DA-3E7BDDA97E7C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {163A89FA-5F97-4332-B5DA-3E7BDDA97E7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {163A89FA-5F97-4332-B5DA-3E7BDDA97E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {163A89FA-5F97-4332-B5DA-3E7BDDA97E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {163A89FA-5F97-4332-B5DA-3E7BDDA97E7C}.Release|Any CPU.Build.0 = Release|Any CPU
{6FBF4FCA-D1E5-4BE3-8BA5-48EF90D8E966}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FBF4FCA-D1E5-4BE3-8BA5-48EF90D8E966}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FBF4FCA-D1E5-4BE3-8BA5-48EF90D8E966}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -56,6 +61,7 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
+ {163A89FA-5F97-4332-B5DA-3E7BDDA97E7C} = {28623E11-D15F-4448-82F6-B86A7D59B1D4}
{6FBF4FCA-D1E5-4BE3-8BA5-48EF90D8E966} = {28623E11-D15F-4448-82F6-B86A7D59B1D4}
{F2750329-F469-4BD8-A0D3-7E1CE2D62E19} = {28623E11-D15F-4448-82F6-B86A7D59B1D4}
{A1F889D2-290B-42C2-A61C-877C97F9D5EB} = {EAC63754-09D3-49C9-ACDF-ECF73AA1D922}
diff --git a/src/ProfanityFilter.Action/ProfanityFilter.Action.csproj b/src/ProfanityFilter.Action/ProfanityFilter.Action.csproj
index 3f9dcc0..53d51d4 100644
--- a/src/ProfanityFilter.Action/ProfanityFilter.Action.csproj
+++ b/src/ProfanityFilter.Action/ProfanityFilter.Action.csproj
@@ -30,7 +30,6 @@
-
diff --git a/src/ProfanityFilter.Services/Data/ProfaneContentReader.cs b/src/ProfanityFilter.Services/Data/ProfaneContentReader.cs
index a9306f9..5cd9e1c 100644
--- a/src/ProfanityFilter.Services/Data/ProfaneContentReader.cs
+++ b/src/ProfanityFilter.Services/Data/ProfaneContentReader.cs
@@ -10,7 +10,7 @@ internal sealed class ProfaneContentReader
EnsureWorkingDirectory();
var builder = new GlobOptionsBuilder()
- .WithPattern("Data/*.txt");
+ .WithPattern("**/Data/*.txt");
return builder.Build();
});
diff --git a/src/ProfanityFilter.Services/DefaultProfaneContentFilterService.cs b/src/ProfanityFilter.Services/DefaultProfaneContentFilterService.cs
index 8931155..1ecab88 100644
--- a/src/ProfanityFilter.Services/DefaultProfaneContentFilterService.cs
+++ b/src/ProfanityFilter.Services/DefaultProfaneContentFilterService.cs
@@ -7,12 +7,8 @@ internal sealed class DefaultProfaneContentFilterService(IMemoryCache cache) : I
{
private const string ProfaneListKey = nameof(ProfaneListKey);
- ///
- /// Reads all profane words from their respective sources asynchronously.
- ///
- /// A representing the asynchronous operation that
- /// returns a readonly dictionary of all profane words.
- private async Task> ReadAllProfaneWordsAsync()
+ ///
+ public async Task> ReadAllProfaneWordsAsync()
{
return await cache.GetOrCreateAsync(ProfaneListKey, async entry =>
{
@@ -62,7 +58,7 @@ await Parallel.ForEachAsync(fileNames,
}
///
- async ValueTask IProfaneContentFilterService.FilterProfanityAsync(
+ public async ValueTask FilterProfanityAsync(
string content,
FilterParameters parameters)
{
diff --git a/src/ProfanityFilter.Services/Filters/ProfaneSourceFilter.cs b/src/ProfanityFilter.Services/Filters/ProfaneSourceFilter.cs
index ee44518..c623f22 100644
--- a/src/ProfanityFilter.Services/Filters/ProfaneSourceFilter.cs
+++ b/src/ProfanityFilter.Services/Filters/ProfaneSourceFilter.cs
@@ -16,6 +16,7 @@ public record class ProfaneSourceFilter(
/// Gets the string representation of the set
/// represented as a regular expression pattern.
///
+ [JsonIgnore]
public string RegexPattern { get; } =
$"\\b({string.Join('|', ProfaneWords)})\\b";
}
diff --git a/src/ProfanityFilter.Services/Filters/ReplacementStrategy.cs b/src/ProfanityFilter.Services/Filters/ReplacementStrategy.cs
index 9c288d9..f593319 100644
--- a/src/ProfanityFilter.Services/Filters/ReplacementStrategy.cs
+++ b/src/ProfanityFilter.Services/Filters/ReplacementStrategy.cs
@@ -42,7 +42,7 @@ public enum ReplacementStrategy
MiddleAsterisk,
///
- /// Represents the first letter then asterisk replacement strategy, which replaces the
+ /// Represents the first letter then asterisk replacement strategy, which replaces
/// everything after the first letter of the profanity with asterisk.
///
FirstLetterThenAsterisk,
diff --git a/src/ProfanityFilter.Services/GlobalUsings.cs b/src/ProfanityFilter.Services/GlobalUsings.cs
index 2e8af19..179bc6c 100644
--- a/src/ProfanityFilter.Services/GlobalUsings.cs
+++ b/src/ProfanityFilter.Services/GlobalUsings.cs
@@ -6,6 +6,7 @@
global using System.Diagnostics.CodeAnalysis;
global using System.Text;
global using System.Text.RegularExpressions;
+global using System.Text.Json.Serialization;
global using Microsoft.Extensions.Caching.Memory;
global using Microsoft.Extensions.DependencyInjection;
diff --git a/src/ProfanityFilter.Services/IProfaneContentFilterService.cs b/src/ProfanityFilter.Services/IProfaneContentFilterService.cs
index 536d02c..ad7afb1 100644
--- a/src/ProfanityFilter.Services/IProfaneContentFilterService.cs
+++ b/src/ProfanityFilter.Services/IProfaneContentFilterService.cs
@@ -18,4 +18,11 @@ public interface IProfaneContentFilterService
ValueTask FilterProfanityAsync(
string content,
FilterParameters parameters);
+
+ ///
+ /// Reads all profane words from their respective sources asynchronously.
+ ///
+ /// A representing the asynchronous operation that
+ /// returns a readonly dictionary of all profane words.
+ Task> ReadAllProfaneWordsAsync();
}
diff --git a/src/ProfanityFilter.WebApi/Endpoints/ProfanityFilterEndpointExtensions.cs b/src/ProfanityFilter.WebApi/Endpoints/ProfanityFilterEndpointExtensions.cs
new file mode 100644
index 0000000..c56ccc9
--- /dev/null
+++ b/src/ProfanityFilter.WebApi/Endpoints/ProfanityFilterEndpointExtensions.cs
@@ -0,0 +1,120 @@
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace ProfanityFilter.WebApi.Endpoints;
+
+internal static class ProfanityFilterEndpointExtensions
+{
+ internal static WebApplication MapProfanityFilterEndpoints(this WebApplication app)
+ {
+ var profanity = app.MapGroup("profanity");
+
+ profanity.MapPost("filter", OnApplyFilterAsync)
+ .WithOpenApi()
+ .WithRequestTimeout(TimeSpan.FromSeconds(10))
+ .WithSummary("""
+ Use this endpoint to attempt applying a profanity-filter. The response is returned as Markdown.
+ """)
+ .WithHttpLogging(HttpLoggingFields.All);
+
+ profanity.MapGet("strategies", OnGetStrategies)
+ .WithOpenApi()
+ .WithRequestTimeout(TimeSpan.FromSeconds(10))
+ .CacheOutput()
+ .WithSummary("""
+ Returns an array of the possible replacement strategies available. See https://github.com/IEvangelist/profanity-filter?tab=readme-ov-file#-replacement-strategies
+ """)
+ .WithHttpLogging(HttpLoggingFields.All);
+
+ var data = profanity.MapGroup("data");
+
+ data.MapGet("names", OnGetDataNamesAsync)
+ .WithOpenApi()
+ .WithRequestTimeout(TimeSpan.FromSeconds(10))
+ .CacheOutput()
+ .WithSummary("""
+ Returns an array of the data names.
+ """)
+ .WithHttpLogging(HttpLoggingFields.All);
+
+ data.MapGet("{name}", OnGetDataByNameAsync)
+ .WithOpenApi()
+ .WithRequestTimeout(TimeSpan.FromSeconds(10))
+ .CacheOutput()
+ .WithSummary("""
+ Returns an array of the profane words for a given data name.
+ """)
+ .WithHttpLogging(HttpLoggingFields.All);
+
+ return app;
+ }
+
+ private static async Task OnApplyFilterAsync(
+ [FromBody] ProfanityFilterRequest request,
+ [FromServices] IProfaneContentFilterService filterService)
+ {
+ if (request is null || string.IsNullOrWhiteSpace(request.Text))
+ {
+ return Results.BadRequest($"""
+ You need to provide a valid request.
+ """);
+ }
+
+ var parameters = new FilterParameters(
+ request.Strategy, FilterTarget.Body);
+
+ var filterResult =
+ await filterService.FilterProfanityAsync(request.Text, parameters);
+
+ var response = new ProfanityFilterResponse(
+ ContainsProfanity: filterResult.IsFiltered,
+ InputText: filterResult.Input,
+ FilteredText: filterResult.FinalOutput,
+ ReplacementStrategy: request.Strategy,
+ FiltrationSteps: [.. filterResult.Steps?.Where(static s => s.IsFiltered)],
+ Matches: [.. filterResult.Matches]
+ );
+
+ return TypedResults.Json(
+ response,
+ SourceGenerationContext.Default.ProfanityFilterResponse);
+ }
+
+ private static IResult OnGetStrategies() =>
+ TypedResults.Json([
+ .. Enum.GetValues()
+ ],
+ SourceGenerationContext.Default.StrategyResponseArray
+ );
+
+ private static async Task OnGetDataNamesAsync(
+ [FromServices] IProfaneContentFilterService filterService)
+ {
+ var map = await filterService.ReadAllProfaneWordsAsync();
+
+ return TypedResults.Json([
+ .. map.Keys.Select(static key => Path.GetFileNameWithoutExtension(key))
+ ],
+ SourceGenerationContext.Default.StringArray);
+ }
+
+ private static async Task OnGetDataByNameAsync(
+ [FromRoute] string name,
+ [FromServices] IProfaneContentFilterService filterService)
+ {
+ var map = await filterService.ReadAllProfaneWordsAsync();
+
+ foreach (var (key, value) in map)
+ {
+ if (Path.GetFileNameWithoutExtension(key) == name)
+ {
+ return TypedResults.Json([
+ .. value.ProfaneWords
+ ],
+ SourceGenerationContext.Default.StringArray);
+ }
+ }
+
+ return TypedResults.NotFound();
+ }
+}
diff --git a/src/ProfanityFilter.WebApi/GlobalUsings.cs b/src/ProfanityFilter.WebApi/GlobalUsings.cs
new file mode 100644
index 0000000..081e6f9
--- /dev/null
+++ b/src/ProfanityFilter.WebApi/GlobalUsings.cs
@@ -0,0 +1,19 @@
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+global using System.Diagnostics.CodeAnalysis;
+
+global using System.Text.Json;
+global using System.Text.Json.Serialization;
+
+global using Microsoft.AspNetCore.Mvc;
+global using Microsoft.AspNetCore.HttpLogging;
+
+global using ProfanityFilter.Services;
+global using ProfanityFilter.Services.Extensions;
+global using ProfanityFilter.Services.Filters;
+global using ProfanityFilter.Services.Results;
+
+global using ProfanityFilter.WebApi.Endpoints;
+global using ProfanityFilter.WebApi.Models;
+global using ProfanityFilter.WebApi.Serialization;
diff --git a/src/ProfanityFilter.WebApi/Models/ProfanityFilterRequest.cs b/src/ProfanityFilter.WebApi/Models/ProfanityFilterRequest.cs
new file mode 100644
index 0000000..f4561f9
--- /dev/null
+++ b/src/ProfanityFilter.WebApi/Models/ProfanityFilterRequest.cs
@@ -0,0 +1,13 @@
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace ProfanityFilter.WebApi.Models;
+
+///
+/// A representation of a profanity-filter request.
+///
+/// The text to evaluate for profanity.
+/// The desired replacement strategy to use. Defaults to *.
+public sealed record class ProfanityFilterRequest(
+ string Text,
+ ReplacementStrategy Strategy = ReplacementStrategy.Asterisk);
diff --git a/src/ProfanityFilter.WebApi/Models/ProfanityFilterResponse.cs b/src/ProfanityFilter.WebApi/Models/ProfanityFilterResponse.cs
new file mode 100644
index 0000000..14b0341
--- /dev/null
+++ b/src/ProfanityFilter.WebApi/Models/ProfanityFilterResponse.cs
@@ -0,0 +1,27 @@
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace ProfanityFilter.WebApi.Models;
+
+///
+/// A representation of a profanity-filter response object.
+///
+/// A boolean value indicating whether or
+/// not the the contains profanity.
+/// The original input text.
+/// The final output text, after filters have been applied.
+/// The replacement strategy used.
+/// An array of steps representing the
+/// filtration process, step-by-step.
+public sealed record class ProfanityFilterResponse(
+ [property:MemberNotNullWhen(
+ true,
+ nameof(ProfanityFilterResponse.FilteredText),
+ nameof(ProfanityFilterResponse.FiltrationSteps),
+ nameof(ProfanityFilterResponse.Matches))]
+ bool ContainsProfanity,
+ string InputText,
+ string? FilteredText,
+ ReplacementStrategy ReplacementStrategy,
+ ProfanityFilterStep[]? FiltrationSteps = default,
+ string[]? Matches = default);
diff --git a/src/ProfanityFilter.WebApi/Models/ProfanityFilterStep.cs b/src/ProfanityFilter.WebApi/Models/ProfanityFilterStep.cs
new file mode 100644
index 0000000..ff71c0e
--- /dev/null
+++ b/src/ProfanityFilter.WebApi/Models/ProfanityFilterStep.cs
@@ -0,0 +1,20 @@
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace ProfanityFilter.WebApi.Models;
+
+///
+/// A representation of a profanity-filter step, detailing
+/// each step applied to a filter operation.
+///
+/// The input before the step ran.
+/// The output after the step ran.
+/// The profane source data for the step. For example, Data/SwearWordList.txt.
+public sealed record class ProfanityFilterStep(
+ string Input,
+ string Output,
+ string ProfaneSourceData)
+{
+ public static implicit operator ProfanityFilterStep(FilterStep step) =>
+ new(step.Input, step.Output!, step.ProfaneSourceData);
+}
diff --git a/src/ProfanityFilter.WebApi/Models/StrategyResponse.cs b/src/ProfanityFilter.WebApi/Models/StrategyResponse.cs
new file mode 100644
index 0000000..fb1aecc
--- /dev/null
+++ b/src/ProfanityFilter.WebApi/Models/StrategyResponse.cs
@@ -0,0 +1,43 @@
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace ProfanityFilter.WebApi.Models;
+
+///
+/// A representation of a strategy response object.
+///
+/// The name of the strategy.
+/// The int value of the strategy.
+/// The description of the strategy.
+public sealed record class StrategyResponse(
+ string StrategyName,
+ int StrategyValue,
+ string Description)
+{
+ public static implicit operator StrategyResponse(ReplacementStrategy strategy)
+ {
+ var description = strategy switch
+ {
+ ReplacementStrategy.Asterisk => "Replaces the profanity word with asterisk.",
+ ReplacementStrategy.Emoji => "Replaces the profanity word with an emoji.",
+ ReplacementStrategy.AngerEmoji => "Replaces the profanity word with the one of the anger emoji.",
+ ReplacementStrategy.MiddleSwearEmoji => "Represents a replacement strategy where the middle of the swear word is replaced with an emoji.",
+ ReplacementStrategy.RandomAsterisk => "Represents a replacement strategy where the profanity is replaced with a random number of asterisk.",
+ ReplacementStrategy.MiddleAsterisk => "Represents the middle asterisk replacement strategy, which replaces the characters in the middle of the profanity with asterisk.",
+ ReplacementStrategy.FirstLetterThenAsterisk => "Represents the first letter then asterisk replacement strategy, which replaces everything after the first letter of the profanity with asterisk.",
+ ReplacementStrategy.VowelAsterisk => "Represents a replacement strategy where vowels in a profane wordare replaced with asterisk.",
+ ReplacementStrategy.Bleep => "Represents a replacement strategy where the profanity is replaced with athe word \"bleep\".",
+ ReplacementStrategy.RedactedRectangle => "Represents a replacement strategy where the profane word has each letterreplaced with the rectangle symbol █.",
+ ReplacementStrategy.StrikeThrough => "Represents a replacement strategy where the profane word is ~~struck through~~.",
+ ReplacementStrategy.Underscores => "Represents a replacement strategy where the profane word is replaced by underscores.",
+ ReplacementStrategy.Grawlix => "Represents a replacement strategy where the profane word is replaced by grawlix, for example \"#$@!\".",
+ ReplacementStrategy.BoldGrawlix => "Represents a replacement strategy where the profane word is replaced by bold grawlix, for example \"#$@!\".",
+ _ => "",
+ };
+
+ return new StrategyResponse(
+ StrategyName: Enum.GetName(strategy) ?? strategy.ToString(),
+ StrategyValue: (int)strategy,
+ Description: description);
+ }
+}
diff --git a/src/ProfanityFilter.WebApi/ProfanityFilter.WebApi.csproj b/src/ProfanityFilter.WebApi/ProfanityFilter.WebApi.csproj
new file mode 100644
index 0000000..3914f90
--- /dev/null
+++ b/src/ProfanityFilter.WebApi/ProfanityFilter.WebApi.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0
+ enable
+ enable
+ linux-x64
+ True
+ c381930b-4a15-4c4d-8d19-b5070cbb9fd6
+ Linux
+ ..\..
+
+
+
+
+
+
+
+
+
+ 8081
+
+
+
+
+
+
+
+
diff --git a/src/ProfanityFilter.WebApi/Program.cs b/src/ProfanityFilter.WebApi/Program.cs
new file mode 100644
index 0000000..dbdc0da
--- /dev/null
+++ b/src/ProfanityFilter.WebApi/Program.cs
@@ -0,0 +1,26 @@
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+builder.Services.AddProfanityFilterServices();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.MapProfanityFilterEndpoints();
+
+app.Run();
diff --git a/src/ProfanityFilter.WebApi/Serialization/SourceGeneratorContext.cs b/src/ProfanityFilter.WebApi/Serialization/SourceGeneratorContext.cs
new file mode 100644
index 0000000..e7de915
--- /dev/null
+++ b/src/ProfanityFilter.WebApi/Serialization/SourceGeneratorContext.cs
@@ -0,0 +1,20 @@
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace ProfanityFilter.WebApi.Serialization;
+
+[JsonSourceGenerationOptions(
+ defaults: JsonSerializerDefaults.Web,
+ WriteIndented = true,
+ UseStringEnumConverter = true,
+ AllowTrailingCommas = true,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString,
+ PropertyNameCaseInsensitive = false,
+ IncludeFields = true)]
+[JsonSerializable(typeof(ProfanityFilterRequest))]
+[JsonSerializable(typeof(ProfanityFilterResponse))]
+[JsonSerializable(typeof(StrategyResponse))]
+[JsonSerializable(typeof(StrategyResponse[]))]
+internal partial class SourceGenerationContext : JsonSerializerContext
+{
+}
\ No newline at end of file
diff --git a/src/ProfanityFilter.WebApi/appsettings.Development.json b/src/ProfanityFilter.WebApi/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/src/ProfanityFilter.WebApi/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/ProfanityFilter.WebApi/appsettings.json b/src/ProfanityFilter.WebApi/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/src/ProfanityFilter.WebApi/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/tests/ProfanityFilter.Action.Tests/ProfanityFilter.Action.Tests.csproj b/tests/ProfanityFilter.Action.Tests/ProfanityFilter.Action.Tests.csproj
index b5ac64a..b7161ce 100644
--- a/tests/ProfanityFilter.Action.Tests/ProfanityFilter.Action.Tests.csproj
+++ b/tests/ProfanityFilter.Action.Tests/ProfanityFilter.Action.Tests.csproj
@@ -16,7 +16,10 @@
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/tests/ProfanityFilter.Action.Tests/TestProfanityFilterService.cs b/tests/ProfanityFilter.Action.Tests/TestProfanityFilterService.cs
index b83122c..cccaf22 100644
--- a/tests/ProfanityFilter.Action.Tests/TestProfanityFilterService.cs
+++ b/tests/ProfanityFilter.Action.Tests/TestProfanityFilterService.cs
@@ -9,4 +9,9 @@ public ValueTask FilterProfanityAsync(string content, FilterParame
{
throw new NotImplementedException();
}
+
+ public Task> ReadAllProfaneWordsAsync()
+ {
+ throw new NotImplementedException();
+ }
}
diff --git a/tests/ProfanityFilter.Services.Tests/ProfanityFilter.Services.Tests.csproj b/tests/ProfanityFilter.Services.Tests/ProfanityFilter.Services.Tests.csproj
index 716b401..d6d9a30 100644
--- a/tests/ProfanityFilter.Services.Tests/ProfanityFilter.Services.Tests.csproj
+++ b/tests/ProfanityFilter.Services.Tests/ProfanityFilter.Services.Tests.csproj
@@ -16,7 +16,10 @@
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+