From 8cc4cfdf0e516df46f92116b1b481ec4dda09600 Mon Sep 17 00:00:00 2001 From: Stratis Dermanoutsos Date: Thu, 18 Sep 2025 11:05:39 +0300 Subject: [PATCH 1/9] Created plugin --- Directory.Packages.props | 3 ++ StringTemplates.slnx | 4 +- examples/StringTemplates.Demo/Program.cs | 6 ++- .../StringTemplates.Demo.csproj | 1 + .../StringTemplates.Demo/StringTemplates.http | 8 ++-- .../ConfigurationTemplatePlugin.cs | 24 ++++++++++++ ...ringTemplates.Plugins.Configuration.csproj | 37 +++++++++++++++++++ .../Services/SystemServiceTests.cs | 18 ++++----- 8 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs create mode 100644 src/plugins/StringTemplates.Plugins.Configuration/StringTemplates.Plugins.Configuration.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index 22956ca..b02579a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,10 +12,13 @@ + + + diff --git a/StringTemplates.slnx b/StringTemplates.slnx index d2cf459..5538a1a 100644 --- a/StringTemplates.slnx +++ b/StringTemplates.slnx @@ -19,7 +19,9 @@ - + + + diff --git a/examples/StringTemplates.Demo/Program.cs b/examples/StringTemplates.Demo/Program.cs index a6eb318..76c19a5 100644 --- a/examples/StringTemplates.Demo/Program.cs +++ b/examples/StringTemplates.Demo/Program.cs @@ -1,11 +1,13 @@ using Microsoft.AspNetCore.Mvc; using StringTemplates.Extensions; +using StringTemplates.Plugins.Configuration; using StringTemplates.Services; var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddStringTemplates(); +builder.Services.AddStringTemplates(options => options.AddPlugins(opts => + opts.AddPluginSingleton())); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); @@ -19,7 +21,7 @@ } // Map endpoints -app.MapPost("system", ( +app.MapPost("general", ( [FromBody] SystemRequest request, [FromServices] ITemplateService templateService) => Results.Ok(templateService.ReplacePlaceholders(request.Template))); diff --git a/examples/StringTemplates.Demo/StringTemplates.Demo.csproj b/examples/StringTemplates.Demo/StringTemplates.Demo.csproj index 263b40d..a8107ae 100644 --- a/examples/StringTemplates.Demo/StringTemplates.Demo.csproj +++ b/examples/StringTemplates.Demo/StringTemplates.Demo.csproj @@ -5,6 +5,7 @@ + diff --git a/examples/StringTemplates.Demo/StringTemplates.http b/examples/StringTemplates.Demo/StringTemplates.http index 5368867..c0d2163 100644 --- a/examples/StringTemplates.Demo/StringTemplates.http +++ b/examples/StringTemplates.Demo/StringTemplates.http @@ -1,5 +1,5 @@ -### POST 1 request to use system template -POST https://localhost:7228/system +### POST 1 request to use generic plugins +POST https://localhost:7228/general Content-Type: application/json { @@ -7,8 +7,8 @@ Content-Type: application/json } ### -### POST 2 request to use system template -POST https://localhost:7228/system +### POST 2 request to use generic plugins +POST https://localhost:7228/general Content-Type: application/json { diff --git a/src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs b/src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs new file mode 100644 index 0000000..c0c36e1 --- /dev/null +++ b/src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Configuration; +using StringTemplates.Services; + +namespace StringTemplates.Plugins.Configuration; + +/// +/// implementation for handling configuration values inside of template strings. +///

+/// Placeholders are detected using the Configuration tag, E.g. +/// +/// {{#Configuration#ConnectionStrings:Database#Configuration#}} +/// {{#Configuration#KeyVault:Auth:ClientId#Configuration#}} +/// +///
+/// The application's instance to read values from. +public sealed class ConfigurationTemplatePlugin(IConfiguration configuration) : ITemplatePlugin +{ + public string PlaceholderTag => "Configuration"; + + public string? GetValueOrDefault(string placeholder) + { + return configuration[placeholder]; + } +} diff --git a/src/plugins/StringTemplates.Plugins.Configuration/StringTemplates.Plugins.Configuration.csproj b/src/plugins/StringTemplates.Plugins.Configuration/StringTemplates.Plugins.Configuration.csproj new file mode 100644 index 0000000..04df886 --- /dev/null +++ b/src/plugins/StringTemplates.Plugins.Configuration/StringTemplates.Plugins.Configuration.csproj @@ -0,0 +1,37 @@ + + + + StringTemplates.Configuration + StringTemplates.Configuration + 9.0.9 + + Stratis-Dermanoutsos + Stratis-Dermanoutsos + + Copyright (c) Stratis Dermanoutsos 2025 + LICENSE + + https://github.com/Stratis-OSS/StringTemplates.Net + https://github.com/Stratis-OSS/StringTemplates.Net + + The best way to handle templates and replace placeholders using IConfiguration. + open-source oss csharp dotnet strings templates placeholders environment config + README.md + * First release. + * IConfiguration plugin. + + + true + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/StringTemplates.IntegrationTests/Services/SystemServiceTests.cs b/tests/StringTemplates.IntegrationTests/Services/SystemServiceTests.cs index 5b84634..179fc6a 100644 --- a/tests/StringTemplates.IntegrationTests/Services/SystemServiceTests.cs +++ b/tests/StringTemplates.IntegrationTests/Services/SystemServiceTests.cs @@ -14,7 +14,7 @@ public async Task System_Date_Now_Replaced() var payload = new { template = "Today is {{#System#Date.Now#System#}}." }; // Act - var result = await PostForStringAsync("system", payload); + var result = await PostForStringAsync("general", payload); // Assert result.ShouldBe($"Today is {expected}."); @@ -28,7 +28,7 @@ public async Task System_Date_UtcNow_Replaced() var payload = new { template = "UTC date: {{#System#Date.UtcNow#System#}}" }; // Act - var result = await PostForStringAsync("system", payload); + var result = await PostForStringAsync("general", payload); // Assert result.ShouldBe($"UTC date: {expected}"); @@ -41,7 +41,7 @@ public async Task System_DateTime_Now_Format_Is_Correct() var payload = new { template = "[{{#System#DateTime.Now#System#}}]" }; // Act - var result = await PostForStringAsync("system", payload); + var result = await PostForStringAsync("general", payload); // Assert result.ShouldMatch(@"^\[\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}\]$"); @@ -54,7 +54,7 @@ public async Task System_DateTime_UtcNow_Format_Is_Correct() var payload = new { template = "<{{#System#DateTime.UtcNow#System#}}>" }; // Act - var result = await PostForStringAsync("system", payload); + var result = await PostForStringAsync("general", payload); // Assert result.ShouldMatch(@"^<\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}>$"); @@ -71,8 +71,8 @@ public async Task System_Day_And_Month_Replaced() var monthPayload = new { template = "It is {{#System#Month#System#}} now." }; // Act - var dayResult = await PostForStringAsync("system", dayPayload); - var monthResult = await PostForStringAsync("system", monthPayload); + var dayResult = await PostForStringAsync("general", dayPayload); + var monthResult = await PostForStringAsync("general", monthPayload); // Assert dayResult.ShouldBe($"Happy {expectedDay}!"); @@ -86,7 +86,7 @@ public async Task System_Time_Now_Format_Is_Correct() var payload = new { template = "time={{#System#Time.Now#System#}}" }; // Act - var result = await PostForStringAsync("system", payload); + var result = await PostForStringAsync("general", payload); // Assert result.ShouldMatch(@"^time=\d{2}:\d{2}:\d{2}$"); @@ -99,7 +99,7 @@ public async Task System_Time_UtcNow_Format_Is_Correct() var payload = new { template = "utc={{#System#Time.UtcNow#System#}}" }; // Act - var result = await PostForStringAsync("system", payload); + var result = await PostForStringAsync("general", payload); // Assert result.ShouldMatch(@"^utc=\d{2}:\d{2}:\d{2}$"); @@ -113,7 +113,7 @@ public async Task System_Unknown_Placeholder_Remains() var payload = new { template }; // Act - var result = await PostForStringAsync("system", payload); + var result = await PostForStringAsync("general", payload); // Assert result.ShouldBe(template); From eb17e08886bd84e6f1416838e15970f1cbbbde37 Mon Sep 17 00:00:00 2001 From: Stratis Dermanoutsos Date: Fri, 19 Sep 2025 08:10:51 +0300 Subject: [PATCH 2/9] Finished Configuration plugin --- examples/StringTemplates.Demo/Program.cs | 2 +- examples/StringTemplates.Demo/StringTemplates.http | 9 +++++++++ examples/StringTemplates.Demo/appsettings.json | 4 ++++ .../ConfigurationTemplatePlugin.cs | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/StringTemplates.Demo/Program.cs b/examples/StringTemplates.Demo/Program.cs index 76c19a5..75e7802 100644 --- a/examples/StringTemplates.Demo/Program.cs +++ b/examples/StringTemplates.Demo/Program.cs @@ -7,7 +7,7 @@ // Add services to the container. builder.Services.AddStringTemplates(options => options.AddPlugins(opts => - opts.AddPluginSingleton())); + opts.AddPluginSingleton())); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); diff --git a/examples/StringTemplates.Demo/StringTemplates.http b/examples/StringTemplates.Demo/StringTemplates.http index c0d2163..31d1de9 100644 --- a/examples/StringTemplates.Demo/StringTemplates.http +++ b/examples/StringTemplates.Demo/StringTemplates.http @@ -15,4 +15,13 @@ Content-Type: application/json "template": "The time and date now is \u007B\u007B#System#Time.Now#System#\u007D\u007D of \u007B\u007B#System#Day.OfMonth#System#\u007D\u007Dth of \u007B\u007B#System#Month#System#\u007D\u007D \u007B\u007B#System#Year#System#\u007D\u007D." } ### + +### POST 3 request to use generic plugins +POST https://localhost:7228/general +Content-Type: application/json + +{ + "template": "This project was created in \u007B\u007B#System#Year#System#\u007D\u007D and the database used is '\u007B\u007B#Configuration#ConnectionStrings.DefaultConnection#Configuration#\u007D\u007D'." +} +### ### diff --git a/examples/StringTemplates.Demo/appsettings.json b/examples/StringTemplates.Demo/appsettings.json index 10f68b8..8503740 100644 --- a/examples/StringTemplates.Demo/appsettings.json +++ b/examples/StringTemplates.Demo/appsettings.json @@ -5,5 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, + "Random:Value": "This is a random value from appsettings.json", + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-StringTemplates.Demo;Trusted_Connection=True;MultipleActiveResultSets=true" + }, "AllowedHosts": "*" } diff --git a/src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs b/src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs index c0c36e1..3e1f7de 100644 --- a/src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs +++ b/src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs @@ -19,6 +19,6 @@ public sealed class ConfigurationTemplatePlugin(IConfiguration configuration) : public string? GetValueOrDefault(string placeholder) { - return configuration[placeholder]; + return configuration[placeholder.Replace(".", ":")]; } } From c284bb49a444d38b2905d2ce167647f13e9572e2 Mon Sep 17 00:00:00 2001 From: Stratis Dermanoutsos Date: Fri, 19 Sep 2025 08:21:43 +0300 Subject: [PATCH 3/9] More modular test infrastructure --- StringTemplates.slnx | 2 ++ .../ApiFactory.cs | 2 +- .../Services/Abstractions/ApiTests.cs | 3 ++- ...tringTemplates.IntegrationTests.Common.csproj | 16 ++++++++++++++++ .../Services/DictionaryServiceTests.cs | 2 +- .../Services/SystemServiceTests.cs | 2 +- .../StringTemplates.IntegrationTests.csproj | 3 +-- 7 files changed, 24 insertions(+), 6 deletions(-) rename tests/{StringTemplates.IntegrationTests => StringTemplates.IntegrationTests.Common}/ApiFactory.cs (82%) rename tests/{StringTemplates.IntegrationTests => StringTemplates.IntegrationTests.Common}/Services/Abstractions/ApiTests.cs (81%) create mode 100644 tests/StringTemplates.IntegrationTests.Common/StringTemplates.IntegrationTests.Common.csproj diff --git a/StringTemplates.slnx b/StringTemplates.slnx index 5538a1a..b601858 100644 --- a/StringTemplates.slnx +++ b/StringTemplates.slnx @@ -23,7 +23,9 @@
+ + \ No newline at end of file diff --git a/tests/StringTemplates.IntegrationTests/ApiFactory.cs b/tests/StringTemplates.IntegrationTests.Common/ApiFactory.cs similarity index 82% rename from tests/StringTemplates.IntegrationTests/ApiFactory.cs rename to tests/StringTemplates.IntegrationTests.Common/ApiFactory.cs index d632876..d541f06 100644 --- a/tests/StringTemplates.IntegrationTests/ApiFactory.cs +++ b/tests/StringTemplates.IntegrationTests.Common/ApiFactory.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.Testing; -namespace StringTemplates.IntegrationTests; +namespace StringTemplates.IntegrationTests.Common; public class ApiFactory : WebApplicationFactory { diff --git a/tests/StringTemplates.IntegrationTests/Services/Abstractions/ApiTests.cs b/tests/StringTemplates.IntegrationTests.Common/Services/Abstractions/ApiTests.cs similarity index 81% rename from tests/StringTemplates.IntegrationTests/Services/Abstractions/ApiTests.cs rename to tests/StringTemplates.IntegrationTests.Common/Services/Abstractions/ApiTests.cs index d123f77..99280ae 100644 --- a/tests/StringTemplates.IntegrationTests/Services/Abstractions/ApiTests.cs +++ b/tests/StringTemplates.IntegrationTests.Common/Services/Abstractions/ApiTests.cs @@ -1,6 +1,7 @@ using System.Net.Http.Json; -namespace StringTemplates.IntegrationTests.Services.Abstractions; +// ReSharper disable once CheckNamespace +namespace StringTemplates.IntegrationTests.Common; public abstract class ApiTests(ApiFactory factory) { diff --git a/tests/StringTemplates.IntegrationTests.Common/StringTemplates.IntegrationTests.Common.csproj b/tests/StringTemplates.IntegrationTests.Common/StringTemplates.IntegrationTests.Common.csproj new file mode 100644 index 0000000..fe18f10 --- /dev/null +++ b/tests/StringTemplates.IntegrationTests.Common/StringTemplates.IntegrationTests.Common.csproj @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/tests/StringTemplates.IntegrationTests/Services/DictionaryServiceTests.cs b/tests/StringTemplates.IntegrationTests/Services/DictionaryServiceTests.cs index 785369e..6542e68 100644 --- a/tests/StringTemplates.IntegrationTests/Services/DictionaryServiceTests.cs +++ b/tests/StringTemplates.IntegrationTests/Services/DictionaryServiceTests.cs @@ -1,5 +1,5 @@ using Shouldly; -using StringTemplates.IntegrationTests.Services.Abstractions; +using StringTemplates.IntegrationTests.Common; namespace StringTemplates.IntegrationTests.Services; diff --git a/tests/StringTemplates.IntegrationTests/Services/SystemServiceTests.cs b/tests/StringTemplates.IntegrationTests/Services/SystemServiceTests.cs index 179fc6a..b15bfbe 100644 --- a/tests/StringTemplates.IntegrationTests/Services/SystemServiceTests.cs +++ b/tests/StringTemplates.IntegrationTests/Services/SystemServiceTests.cs @@ -1,6 +1,6 @@ using System.Globalization; using Shouldly; -using StringTemplates.IntegrationTests.Services.Abstractions; +using StringTemplates.IntegrationTests.Common; namespace StringTemplates.IntegrationTests.Services; diff --git a/tests/StringTemplates.IntegrationTests/StringTemplates.IntegrationTests.csproj b/tests/StringTemplates.IntegrationTests/StringTemplates.IntegrationTests.csproj index 1483151..ab26cd6 100644 --- a/tests/StringTemplates.IntegrationTests/StringTemplates.IntegrationTests.csproj +++ b/tests/StringTemplates.IntegrationTests/StringTemplates.IntegrationTests.csproj @@ -6,7 +6,6 @@ - @@ -18,8 +17,8 @@ - + \ No newline at end of file From 507a0d84cc2b6631803cd1674d46e962cbd5edfc Mon Sep 17 00:00:00 2001 From: Stratis Dermanoutsos Date: Fri, 19 Sep 2025 17:51:10 +0300 Subject: [PATCH 4/9] Configuration plugin Unit tests --- Directory.Packages.props | 5 ++- StringTemplates.slnx | 3 ++ .../ConfigurationTemplatePluginTests.cs | 44 +++++++++++++++++++ ...tes.Plugins.Configuration.UnitTests.csproj | 24 ++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 tests/plugins/Configuration/StringTemplates.Plugins.Configuration.UnitTests/Services/Plugins/ConfigurationTemplatePluginTests.cs create mode 100644 tests/plugins/Configuration/StringTemplates.Plugins.Configuration.UnitTests/StringTemplates.Plugins.Configuration.UnitTests.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index b02579a..c49a45e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,13 +12,14 @@ + - - + + diff --git a/StringTemplates.slnx b/StringTemplates.slnx index b601858..5bde359 100644 --- a/StringTemplates.slnx +++ b/StringTemplates.slnx @@ -28,4 +28,7 @@ + + + \ No newline at end of file diff --git a/tests/plugins/Configuration/StringTemplates.Plugins.Configuration.UnitTests/Services/Plugins/ConfigurationTemplatePluginTests.cs b/tests/plugins/Configuration/StringTemplates.Plugins.Configuration.UnitTests/Services/Plugins/ConfigurationTemplatePluginTests.cs new file mode 100644 index 0000000..5c8acd4 --- /dev/null +++ b/tests/plugins/Configuration/StringTemplates.Plugins.Configuration.UnitTests/Services/Plugins/ConfigurationTemplatePluginTests.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Configuration; +using Shouldly; + +namespace StringTemplates.Plugins.Configuration.UnitTests.Services.Plugins; + +public class ConfigurationTemplatePluginTests +{ + [Fact] + public void GetValueOrDefault_ReturnsValue_WhenKeyExists() + { + // Arrange + var expected = "Some value"; + var inMemorySettings = new Dictionary + { + { "AppName", expected } + }; + IConfiguration config = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + var plugin = new ConfigurationTemplatePlugin(config); + + // Act + var result = plugin.GetValueOrDefault("AppName"); + + // Assert + result.ShouldBe(expected); + } + + [Fact] + public void GetValueOrDefault_ReturnsNull_WhenKeyDoesNotExist() + { + // Arrange + IConfiguration config = new ConfigurationBuilder() + .AddInMemoryCollection() + .Build(); + var plugin = new ConfigurationTemplatePlugin(config); + + // Act + var result = plugin.GetValueOrDefault("DoesNotExist"); + + // Assert + result.ShouldBeNull(); + } +} diff --git a/tests/plugins/Configuration/StringTemplates.Plugins.Configuration.UnitTests/StringTemplates.Plugins.Configuration.UnitTests.csproj b/tests/plugins/Configuration/StringTemplates.Plugins.Configuration.UnitTests/StringTemplates.Plugins.Configuration.UnitTests.csproj new file mode 100644 index 0000000..d88eaf3 --- /dev/null +++ b/tests/plugins/Configuration/StringTemplates.Plugins.Configuration.UnitTests/StringTemplates.Plugins.Configuration.UnitTests.csproj @@ -0,0 +1,24 @@ + + + + false + + + + + + + + + + + + + + + + + + + + From 2ed5bb573f936cce7449ea9f914e4807eecda530 Mon Sep 17 00:00:00 2001 From: Stratis Dermanoutsos Date: Fri, 19 Sep 2025 18:04:29 +0300 Subject: [PATCH 5/9] Dictionary unit tests with new format --- .../Plugins/DictionaryTemplatePluginTests.cs | 109 ++++++------------ .../StringTemplates.UnitTests.csproj | 1 + 2 files changed, 34 insertions(+), 76 deletions(-) diff --git a/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs b/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs index 04f65ae..08e9301 100644 --- a/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs +++ b/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs @@ -1,5 +1,4 @@ -using System.Globalization; -using StringTemplates.Services; +using Shouldly; using StringTemplates.Services.Plugins; namespace StringTemplates.UnitTests.Services.Plugins; @@ -10,22 +9,25 @@ public class DictionaryTemplatePluginTests public void Dictionary_Replaces_Single_Key() { // Arrange + var expected = "A value"; var dict = new Dictionary { - ["Name"] = "Stratis" + ["Name"] = expected }; - var template = "Hello, {{#Dictionary#Name#Dictionary#}}! Welcome to StringTemplates."; - ITemplateService> sut = new DictionaryTemplatePlugin(); + var plugin = new DictionaryTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template, dict); + var result = plugin.GetValueOrDefault("Name", dict); // Assert - Assert.Equal("Hello, Stratis! Welcome to StringTemplates.", result); + result.ShouldBe(expected); } - [Fact] - public void Dictionary_Replaces_Multiple_Keys() + [Theory] + [InlineData("FirstName", "Alex")] + [InlineData("LastName", "Papadopoulos")] + [InlineData("Role", "Administrator")] + public void Dictionary_Replaces_Multiple_Keys(string key, string expected) { // Arrange var dict = new Dictionary @@ -34,23 +36,20 @@ public void Dictionary_Replaces_Multiple_Keys() ["LastName"] = "Papadopoulos", ["Role"] = "Administrator" }; - var template = - "User: {{#Dictionary#FirstName#Dictionary#}} {{#Dictionary#LastName#Dictionary#}}.\n" + - "Current role: {{#Dictionary#Role#Dictionary#}}."; - ITemplateService> sut = new DictionaryTemplatePlugin(); + var plugin = new DictionaryTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template, dict); + var result = plugin.GetValueOrDefault(key, dict); // Assert - var expected = - "User: Alex Papadopoulos.\n" + - "Current role: Administrator."; - Assert.Equal(expected, result); + result.ShouldBe(expected); } - [Fact] - public void Dictionary_Replaces_Mixed_Types_And_Repeats() + [Theory] + [InlineData("OrderId", "1729")] + [InlineData("Total", "123,45")] // TODO: Culture-specific decimal separator... + [InlineData("Currency", "EUR")] + public void Dictionary_Replaces_Mixed_Types_And_Repeats(string key, object expected) { // Arrange var dict = new Dictionary @@ -59,88 +58,46 @@ public void Dictionary_Replaces_Mixed_Types_And_Repeats() ["Total"] = 123.45m, ["Currency"] = "EUR" }; - var template = - "Order #{{#Dictionary#OrderId#Dictionary#}} has been processed successfully. " + - "Amount charged: {{#Dictionary#Total#Dictionary#}} {{#Dictionary#Currency#Dictionary#}}. " + - "Reference: ORD-{{#Dictionary#OrderId#Dictionary#}}."; - ITemplateService> sut = new DictionaryTemplatePlugin(); + var plugin = new DictionaryTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template, dict); + var result = plugin.GetValueOrDefault(key, dict); // Assert - var totalFormatted = ((decimal)dict["Total"]).ToString(CultureInfo.CurrentCulture); - var expected = - $"Order #1729 has been processed successfully. " + - $"Amount charged: {totalFormatted} EUR. " + - $"Reference: ORD-1729."; - Assert.Equal(expected, result); + result.ShouldBe(expected); } - [Fact] - public void Dictionary_Unknown_Key_Remains() + [Theory] + [InlineData("Known", "OK")] + [InlineData("NotThere", null)] + public void Dictionary_Known_And_Unknown_Keys_Remain_Or_Are_Replaced(string key, object expected) { // Arrange var dict = new Dictionary { ["Known"] = "OK" }; - var template = - "Known: {{#Dictionary#Known#Dictionary#}}; " + - "Unknown: {{#Dictionary#NotThere#Dictionary#}}."; - ITemplateService> sut = new DictionaryTemplatePlugin(); + var plugin = new DictionaryTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template, dict); + var result = plugin.GetValueOrDefault(key, dict); // Assert - var expected = - "Known: OK; " + - "Unknown: {{#Dictionary#NotThere#Dictionary#}}."; - Assert.Equal(expected, result); - } - - [Fact] - public void Dictionary_Replaces_Values_In_Long_Text() - { - // Arrange - var start = new DateTime(2025, 09, 16, 8, 30, 0); - var dict = new Dictionary - { - ["Company"] = "Lygom", - ["Position"] = "Senior Engineer", - ["StartDate"] = start // ToString() is used by the plugin - }; - - var template = - "Congratulations, {{#Dictionary#Position#Dictionary#}} at {{#Dictionary#Company#Dictionary#}}!\n" + - "Your official start date is {{#Dictionary#StartDate#Dictionary#}}. Please check your inbox for onboarding details."; - ITemplateService> sut = new DictionaryTemplatePlugin(); - - // Act - var result = sut.ReplacePlaceholders(template, dict); - - // Assert - var expected = - $"Congratulations, Senior Engineer at Lygom!\n" + - $"Your official start date is {start.ToString()}. Please check your inbox for onboarding details."; - Assert.Equal(expected, result); + result.ShouldBe(expected); } [Fact] public void Dictionary_Leaves_Template_As_Is_When_Model_Is_Null() { // Arrange + string? expected = null; Dictionary? dict = null; - var template = - "Dear {{#Dictionary#FirstName#Dictionary#}}, your ticket {{#Dictionary#TicketId#Dictionary#}} is received."; - - ITemplateService> sut = new DictionaryTemplatePlugin(); + var plugin = new DictionaryTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template, dict); + var result = plugin.GetValueOrDefault("SomeKey", dict); // Assert - Assert.Equal(template, result); + result.ShouldBe(expected); } } diff --git a/tests/StringTemplates.UnitTests/StringTemplates.UnitTests.csproj b/tests/StringTemplates.UnitTests/StringTemplates.UnitTests.csproj index fbd02f9..829dd04 100644 --- a/tests/StringTemplates.UnitTests/StringTemplates.UnitTests.csproj +++ b/tests/StringTemplates.UnitTests/StringTemplates.UnitTests.csproj @@ -7,6 +7,7 @@ + From a6f5e17f0218ca79f5b6ba0635efa778e4802cc5 Mon Sep 17 00:00:00 2001 From: Stratis Dermanoutsos Date: Sat, 20 Sep 2025 12:21:42 +0300 Subject: [PATCH 6/9] Solved warnings --- .../Services/Plugins/DictionaryTemplatePluginTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs b/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs index 08e9301..1b51f39 100644 --- a/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs +++ b/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs @@ -49,7 +49,7 @@ public void Dictionary_Replaces_Multiple_Keys(string key, string expected) [InlineData("OrderId", "1729")] [InlineData("Total", "123,45")] // TODO: Culture-specific decimal separator... [InlineData("Currency", "EUR")] - public void Dictionary_Replaces_Mixed_Types_And_Repeats(string key, object expected) + public void Dictionary_Replaces_Mixed_Types_And_Repeats(string key, string expected) { // Arrange var dict = new Dictionary @@ -70,7 +70,7 @@ public void Dictionary_Replaces_Mixed_Types_And_Repeats(string key, object expec [Theory] [InlineData("Known", "OK")] [InlineData("NotThere", null)] - public void Dictionary_Known_And_Unknown_Keys_Remain_Or_Are_Replaced(string key, object expected) + public void Dictionary_Known_And_Unknown_Keys_Remain_Or_Are_Replaced(string key, string? expected) { // Arrange var dict = new Dictionary From c147e42d0e20249aef67068463474f57dc4103b6 Mon Sep 17 00:00:00 2001 From: Stratis Dermanoutsos Date: Sun, 21 Sep 2025 14:05:57 +0300 Subject: [PATCH 7/9] Update SystemTemplatePluginTests.cs --- .../Plugins/SystemTemplatePluginTests.cs | 92 +++++++++---------- 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/tests/StringTemplates.UnitTests/Services/Plugins/SystemTemplatePluginTests.cs b/tests/StringTemplates.UnitTests/Services/Plugins/SystemTemplatePluginTests.cs index cd4bb56..02316bc 100644 --- a/tests/StringTemplates.UnitTests/Services/Plugins/SystemTemplatePluginTests.cs +++ b/tests/StringTemplates.UnitTests/Services/Plugins/SystemTemplatePluginTests.cs @@ -1,7 +1,8 @@ -using System.Globalization; -using System.Text.RegularExpressions; +using Shouldly; using StringTemplates.Services; using StringTemplates.Services.Plugins; +using System.Globalization; +using System.Text.RegularExpressions; namespace StringTemplates.UnitTests.Services.Plugins; @@ -12,14 +13,13 @@ public void System_Date_Now_Replaced() { // Arrange var expected = DateTime.Now.ToString("d", CultureInfo.InvariantCulture); - var template = "Today is {{#System#Date.Now#System#}}."; - ITemplateService sut = new SystemTemplatePlugin(); + var plugin = new SystemTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template); + var result = plugin.GetValueOrDefault("Date.Now"); // Assert - Assert.Equal($"Today is {expected}.", result); + result.ShouldBe(expected); } [Fact] @@ -27,44 +27,43 @@ public void System_Date_UtcNow_Replaced() { // Arrange var expected = DateTime.UtcNow.ToString("d", CultureInfo.InvariantCulture); - var template = "UTC date: {{#System#Date.UtcNow#System#}}"; - ITemplateService sut = new SystemTemplatePlugin(); + var plugin = new SystemTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template); + var result = plugin.GetValueOrDefault("Date.UtcNow"); // Assert - Assert.Equal($"UTC date: {expected}", result); + result.ShouldBe(expected); } [Fact] public void System_DateTime_Now_Format_Is_Correct() { // Arrange - var template = "[{{#System#DateTime.Now#System#}}]"; - ITemplateService sut = new SystemTemplatePlugin(); + const string expected = @"^\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}$"; + var plugin = new SystemTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template); + var value = plugin.GetValueOrDefault("DateTime.Now"); + var result = Regex.Match(value ?? string.Empty, expected).Success; // Assert - var m = Regex.Match(result, @"^\[\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}\]$"); - Assert.True(m.Success, $"Result '{result}' did not match expected DateTime format."); + result.ShouldBe(true); } [Fact] public void System_DateTime_UtcNow_Format_Is_Correct() { // Arrange - var template = "<{{#System#DateTime.UtcNow#System#}}>"; - ITemplateService sut = new SystemTemplatePlugin(); + const string expected = @"^\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}$"; + var plugin = new SystemTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template); + var value = plugin.GetValueOrDefault("DateTime.UtcNow"); + var result = Regex.Match(value ?? string.Empty, expected).Success; // Assert - var m = Regex.Match(result, @"^<\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}>$"); - Assert.True(m.Success, $"Result '{result}' did not match expected DateTime format."); + result.ShouldBe(true); } [Fact] @@ -72,14 +71,13 @@ public void System_Day_Replaced() { // Arrange var expected = DateTime.Now.DayOfWeek.ToString(); - var template = "Happy {{#System#Day#System#}}!"; - ITemplateService sut = new SystemTemplatePlugin(); + var plugin = new SystemTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template); + var result = plugin.GetValueOrDefault("Day"); // Assert - Assert.Equal($"Happy {expected}!", result); + result.ShouldBe(expected); } [Fact] @@ -87,72 +85,70 @@ public void System_Month_Replaced() { // Arrange var expected = DateTime.Now.ToString("MMMM", CultureInfo.InvariantCulture); - var template = "It is {{#System#Month#System#}} now."; - ITemplateService sut = new SystemTemplatePlugin(); + var plugin = new SystemTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template); + var result = plugin.GetValueOrDefault("Month"); // Assert - Assert.Equal($"It is {expected} now.", result); + result.ShouldBe(expected); } [Fact] public void System_Time_Now_Format_Is_Correct() { // Arrange - var template = "time={{#System#Time.Now#System#}}"; - ITemplateService sut = new SystemTemplatePlugin(); + const string expected = @"^\d{2}:\d{2}:\d{2}$"; + var plugin = new SystemTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template); + var value = plugin.GetValueOrDefault("Time.Now"); + var result = Regex.Match(value ?? string.Empty, expected).Success; // Assert - var m = Regex.Match(result, @"^time=\d{2}:\d{2}:\d{2}$"); - Assert.True(m.Success, $"Result '{result}' did not match expected time format."); + result.ShouldBe(true); } [Fact] public void System_Time_UtcNow_Format_Is_Correct() { // Arrange - var template = "utc={{#System#Time.UtcNow#System#}}"; - ITemplateService sut = new SystemTemplatePlugin(); + const string expected = @"^\d{2}:\d{2}:\d{2}$"; + var plugin = new SystemTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template); + var value = plugin.GetValueOrDefault("Time.UtcNow"); + var result = Regex.Match(value ?? string.Empty, expected).Success; // Assert - var m = Regex.Match(result, @"^utc=\d{2}:\d{2}:\d{2}$"); - Assert.True(m.Success, $"Result '{result}' did not match expected time format."); + result.ShouldBe(true); } [Fact] public void System_Year_Replaced() { // Arrange - var expectedYear = DateTime.Now.Year.ToString(); - var template = "The year is {{#System#Year#System#}} — stay focused."; - ITemplateService sut = new SystemTemplatePlugin(); + var expected = DateTime.Now.Year.ToString(); + var plugin = new SystemTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template); + var result = plugin.GetValueOrDefault("Year"); // Assert - Assert.Equal($"The year is {expectedYear} — stay focused.", result); + result.ShouldBe(expected); } [Fact] - public void System_Unknown_Placeholder_Remains() + public void GetValueOrDefault_ReturnsNull_WhenKeyDoesNotExist() { // Arrange - var template = "Unknown -> {{#System#Does.Not.Exist#System#}} <- keep it"; - ITemplateService sut = new SystemTemplatePlugin(); + string? expected = null; + var plugin = new SystemTemplatePlugin(); // Act - var result = sut.ReplacePlaceholders(template); + var result = plugin.GetValueOrDefault("Does.Not.Exist"); // Assert - Assert.Equal(template, result); + result.ShouldBe(expected); } } From 9aa3485837363f35590fda721d8209231ce48b18 Mon Sep 17 00:00:00 2001 From: Stratis Dermanoutsos Date: Sun, 21 Sep 2025 14:12:53 +0300 Subject: [PATCH 8/9] Added to Configuration plugin's docs --- .../ConfigurationTemplatePlugin.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs b/src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs index 3e1f7de..f23fd22 100644 --- a/src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs +++ b/src/plugins/StringTemplates.Plugins.Configuration/ConfigurationTemplatePlugin.cs @@ -8,11 +8,20 @@ namespace StringTemplates.Plugins.Configuration; ///

/// Placeholders are detected using the Configuration tag, E.g. /// -/// {{#Configuration#ConnectionStrings:Database#Configuration#}} -/// {{#Configuration#KeyVault:Auth:ClientId#Configuration#}} +/// {{#Configuration#ConnectionStrings.Database#Configuration#}} +/// {{#Configuration#KeyVault.Auth.ClientId#Configuration#}} /// /// /// The application's instance to read values from. +/// +/// For internal use only. +/// Not recommended to expose this plugin to outside users as it could be used to expose Environment Variables. +/// +/// +/// Separators in keys can be a dot (.), e.g. ConnectionStrings.Database. +/// The plugin later replaces dots with colons (:) to match the format used by . +/// E.g. ConnectionStrings:Database. +/// public sealed class ConfigurationTemplatePlugin(IConfiguration configuration) : ITemplatePlugin { public string PlaceholderTag => "Configuration"; From 03e35278b1bfcfc4d32fe48e949e261d2951036e Mon Sep 17 00:00:00 2001 From: Stratis Dermanoutsos Date: Sun, 21 Sep 2025 14:35:26 +0300 Subject: [PATCH 9/9] Fixed culture error in tests --- src/StringTemplates/StringTemplates.csproj | 2 +- .../StringTemplates.Plugins.Configuration.csproj | 2 +- .../Services/Plugins/DictionaryTemplatePluginTests.cs | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/StringTemplates/StringTemplates.csproj b/src/StringTemplates/StringTemplates.csproj index 2b9425b..45d20b2 100644 --- a/src/StringTemplates/StringTemplates.csproj +++ b/src/StringTemplates/StringTemplates.csproj @@ -8,7 +8,7 @@ Stratis-Dermanoutsos Stratis-Dermanoutsos - Copyright (c) Stratis Dermanoutsos 2025 + Copyright © Stratis Dermanoutsos 2025 LICENSE https://github.com/Stratis-OSS/StringTemplates.Net diff --git a/src/plugins/StringTemplates.Plugins.Configuration/StringTemplates.Plugins.Configuration.csproj b/src/plugins/StringTemplates.Plugins.Configuration/StringTemplates.Plugins.Configuration.csproj index 04df886..33c0bad 100644 --- a/src/plugins/StringTemplates.Plugins.Configuration/StringTemplates.Plugins.Configuration.csproj +++ b/src/plugins/StringTemplates.Plugins.Configuration/StringTemplates.Plugins.Configuration.csproj @@ -8,7 +8,7 @@ Stratis-Dermanoutsos Stratis-Dermanoutsos - Copyright (c) Stratis Dermanoutsos 2025 + Copyright © Stratis Dermanoutsos 2025 LICENSE https://github.com/Stratis-OSS/StringTemplates.Net diff --git a/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs b/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs index 1b51f39..87dad95 100644 --- a/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs +++ b/tests/StringTemplates.UnitTests/Services/Plugins/DictionaryTemplatePluginTests.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Shouldly; using StringTemplates.Services.Plugins; @@ -47,11 +48,14 @@ public void Dictionary_Replaces_Multiple_Keys(string key, string expected) [Theory] [InlineData("OrderId", "1729")] - [InlineData("Total", "123,45")] // TODO: Culture-specific decimal separator... + [InlineData("Total", "123.45")] [InlineData("Currency", "EUR")] public void Dictionary_Replaces_Mixed_Types_And_Repeats(string key, string expected) { // Arrange + var culture = CultureInfo.InvariantCulture; + CultureInfo.CurrentCulture = culture; + CultureInfo.CurrentUICulture = culture; var dict = new Dictionary { ["OrderId"] = 1729,