From 07875060add81992910c5a4143f21092caca4d6e Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Wed, 17 Jun 2026 06:57:05 +0300 Subject: [PATCH] Add Templating.Razor module --- Directory.Packages.props | 1 + OrchardCoreContrib.Modules.slnx | 2 + .../OrchardCoreContrib.Modules.Web.csproj | 1 + .../Extensions/ServiceCollectionExtensions.cs | 22 ++++++++ .../Manifest.cs | 11 ++++ ...OrchardCoreContrib.Templating.Razor.csproj | 18 +++++++ .../Services/RazorTemplateEngine.cs | 33 ++++++++++++ .../Startup.cs | 9 ++++ .../ServiceCollectionExtensionsTests.cs | 28 ++++++++++ ...dCoreContrib.Templating.Razor.Tests.csproj | 32 ++++++++++++ .../Services/RazorTemplateEngineTests.cs | 51 +++++++++++++++++++ 11 files changed, 208 insertions(+) create mode 100644 src/OrchardCoreContrib.Templating.Razor/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/OrchardCoreContrib.Templating.Razor/Manifest.cs create mode 100644 src/OrchardCoreContrib.Templating.Razor/OrchardCoreContrib.Templating.Razor.csproj create mode 100644 src/OrchardCoreContrib.Templating.Razor/Services/RazorTemplateEngine.cs create mode 100644 src/OrchardCoreContrib.Templating.Razor/Startup.cs create mode 100644 test/OrchardCoreContrib.Templating.Razor.Tests/Extensions/ServiceCollectionExtensionsTests.cs create mode 100644 test/OrchardCoreContrib.Templating.Razor.Tests/OrchardCoreContrib.Templating.Razor.Tests.csproj create mode 100644 test/OrchardCoreContrib.Templating.Razor.Tests/Services/RazorTemplateEngineTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 8ee6b60..6c1d620 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -59,6 +59,7 @@ + diff --git a/OrchardCoreContrib.Modules.slnx b/OrchardCoreContrib.Modules.slnx index 237bedf..2def2b3 100644 --- a/OrchardCoreContrib.Modules.slnx +++ b/OrchardCoreContrib.Modules.slnx @@ -33,6 +33,7 @@ + @@ -47,6 +48,7 @@ + diff --git a/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj b/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj index b1cfaf0..380117d 100644 --- a/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj +++ b/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj @@ -37,6 +37,7 @@ + diff --git a/src/OrchardCoreContrib.Templating.Razor/Extensions/ServiceCollectionExtensions.cs b/src/OrchardCoreContrib.Templating.Razor/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..6613cca --- /dev/null +++ b/src/OrchardCoreContrib.Templating.Razor/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using OrchardCoreContrib.Templating; +using OrchardCoreContrib.Templating.Razor.Services; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Provides extension methods for registering the Razor template engine services in an . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Registers the Razor template engine services in the specified . + /// + /// The to add the services to. + /// The updated . + public static IServiceCollection AddRazorTemplating(this IServiceCollection services) + { + services.AddSingleton(); + + return services; + } +} diff --git a/src/OrchardCoreContrib.Templating.Razor/Manifest.cs b/src/OrchardCoreContrib.Templating.Razor/Manifest.cs new file mode 100644 index 0000000..6b631fc --- /dev/null +++ b/src/OrchardCoreContrib.Templating.Razor/Manifest.cs @@ -0,0 +1,11 @@ +using OrchardCore.Modules.Manifest; +using ManifestConstants = OrchardCoreContrib.Modules.Manifest.ManifestConstants; + +[assembly: Module( + Name = "Razor Templating", + Author = ManifestConstants.Author, + Website = ManifestConstants.Website, + Version = "1.0.0", + Description = "Provides Razor template engine integration.", + Category = "Templating" +)] diff --git a/src/OrchardCoreContrib.Templating.Razor/OrchardCoreContrib.Templating.Razor.csproj b/src/OrchardCoreContrib.Templating.Razor/OrchardCoreContrib.Templating.Razor.csproj new file mode 100644 index 0000000..d9b6923 --- /dev/null +++ b/src/OrchardCoreContrib.Templating.Razor/OrchardCoreContrib.Templating.Razor.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/OrchardCoreContrib.Templating.Razor/Services/RazorTemplateEngine.cs b/src/OrchardCoreContrib.Templating.Razor/Services/RazorTemplateEngine.cs new file mode 100644 index 0000000..3632aab --- /dev/null +++ b/src/OrchardCoreContrib.Templating.Razor/Services/RazorTemplateEngine.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Localization; +using OrchardCoreContrib.Infrastructure; +using RazorLight; + +namespace OrchardCoreContrib.Templating.Razor.Services; + +/// +/// Represents a template engine that uses the Razor templating language. +/// +public class RazorTemplateEngine(IStringLocalizer S) : ITemplateEngine +{ + private readonly RazorLightEngine _engine = new RazorLightEngineBuilder() + .UseMemoryCachingProvider() + .Build(); + + /// + public async Task> RenderAsync(string template, TemplateContext context) + { + Guard.ArgumentNotNullOrEmpty(template, nameof(template)); + Guard.ArgumentNotNull(context, nameof(context)); + + try + { + var result = await _engine.CompileRenderStringAsync(Guid.NewGuid().ToString(), template, context.Model); + + return Result.Success(result); + } + catch (Exception ex) + { + return Result.Failed(S["Rendering razor template failed: {0}", ex.Message]); + } + } +} diff --git a/src/OrchardCoreContrib.Templating.Razor/Startup.cs b/src/OrchardCoreContrib.Templating.Razor/Startup.cs new file mode 100644 index 0000000..35250bd --- /dev/null +++ b/src/OrchardCoreContrib.Templating.Razor/Startup.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Modules; + +namespace OrchardCoreContrib.Templating.Liquid; + +public sealed class Startup : StartupBase +{ + public override void ConfigureServices(IServiceCollection services) => services.AddRazorTemplating(); +} diff --git a/test/OrchardCoreContrib.Templating.Razor.Tests/Extensions/ServiceCollectionExtensionsTests.cs b/test/OrchardCoreContrib.Templating.Razor.Tests/Extensions/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..4a421fd --- /dev/null +++ b/test/OrchardCoreContrib.Templating.Razor.Tests/Extensions/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.DependencyInjection; +using OrchardCoreContrib.Templating.Razor.Services; + +namespace OrchardCoreContrib.Templating.Razor.Tests.Extensions; + +public class ServiceCollectionExtensionsTests +{ + [Fact] + public void AddRazorTemplating_RegistersRazorTemplateEngineAsSingleton() + { + // Arrange + var services = new ServiceCollection() + .AddLogging() + .AddLocalization(); + + // Act + services.AddRazorTemplating(); + + // Assert + using var provider = services.BuildServiceProvider(); + + var first = provider.GetRequiredService(); + var second = provider.GetRequiredService(); + + Assert.IsType(first); + Assert.Same(first, second); + } +} diff --git a/test/OrchardCoreContrib.Templating.Razor.Tests/OrchardCoreContrib.Templating.Razor.Tests.csproj b/test/OrchardCoreContrib.Templating.Razor.Tests/OrchardCoreContrib.Templating.Razor.Tests.csproj new file mode 100644 index 0000000..b3cd9f6 --- /dev/null +++ b/test/OrchardCoreContrib.Templating.Razor.Tests/OrchardCoreContrib.Templating.Razor.Tests.csproj @@ -0,0 +1,32 @@ + + + + true + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + \ No newline at end of file diff --git a/test/OrchardCoreContrib.Templating.Razor.Tests/Services/RazorTemplateEngineTests.cs b/test/OrchardCoreContrib.Templating.Razor.Tests/Services/RazorTemplateEngineTests.cs new file mode 100644 index 0000000..66cd967 --- /dev/null +++ b/test/OrchardCoreContrib.Templating.Razor.Tests/Services/RazorTemplateEngineTests.cs @@ -0,0 +1,51 @@ +using Microsoft.Extensions.Localization; +using Moq; + +namespace OrchardCoreContrib.Templating.Razor.Services.Tests; + +public class RazorTemplateEngineTests +{ + private readonly RazorTemplateEngine _templateEngine; + + public RazorTemplateEngineTests() + { + var stringLocalizerMock = new Mock>(); + + stringLocalizerMock.Setup(localizer => localizer[It.IsAny()]) + .Returns((string k) => new LocalizedString(k, k)); + + stringLocalizerMock.Setup(localizer => localizer[It.IsAny(), It.IsAny()]) + .Returns((string k, object[] a) => new LocalizedString(k, string.Format(k, a))); + + _templateEngine = new RazorTemplateEngine(stringLocalizerMock.Object); + } + + [Fact] + public async Task RenderAsync_ShouldRenderTemplate_WhenTemplateIsValid() + { + // Arrange + var template = "Hello @Model.Name"; + var context = new TemplateContext(new { Name = "World" }); + + // Act + var result = await _templateEngine.RenderAsync(template, context); + + // Assert + Assert.Equal("Hello World", result.Value); + } + + [Fact] + public async Task RenderAsync_ShouldThrowTemplateRenderException_WhenTemplateParsingFails() + { + // Arrange + var template = "@{ var x = ; }"; + var context = new TemplateContext(); + + // Act + var result = await _templateEngine.RenderAsync(template, context); + + // Assert + Assert.NotEmpty(result.Errors); + Assert.StartsWith("Rendering razor template failed:", result.Errors.Single().Message); + } +}