From e9eecf4410db0fa0670d09c92cb5a7c05a01e7e0 Mon Sep 17 00:00:00 2001 From: Richard Tasker Date: Sun, 3 Jan 2021 15:55:45 +0000 Subject: [PATCH 01/11] Add Solution File and Restructure --- Carter.HtmlNegotiator.sln | 48 +++++++++++++++++++ src/Carter.HtmlNegotiator.csproj | 7 --- .../Carter.HtmlNegotiator.csproj | 19 ++++++++ .../DefaultViewLocator.cs | 0 .../HtmlNegotiator.cs | 0 .../IViewLocator.cs | 0 6 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 Carter.HtmlNegotiator.sln delete mode 100644 src/Carter.HtmlNegotiator.csproj create mode 100644 src/Carter.HtmlNegotiator/Carter.HtmlNegotiator.csproj rename src/{ => Carter.HtmlNegotiator}/DefaultViewLocator.cs (100%) rename src/{ => Carter.HtmlNegotiator}/HtmlNegotiator.cs (100%) rename src/{ => Carter.HtmlNegotiator}/IViewLocator.cs (100%) diff --git a/Carter.HtmlNegotiator.sln b/Carter.HtmlNegotiator.sln new file mode 100644 index 0000000..4a4ab16 --- /dev/null +++ b/Carter.HtmlNegotiator.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carter.HtmlNegotiator", "src\Carter.HtmlNegotiator\Carter.HtmlNegotiator.csproj", "{486553EF-81DD-401C-AA24-A7A360DDC1F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carter", "dependencies\Carter\src\Carter.csproj", "{12FAB6F8-C688-428B-9479-28F8C114A491}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Debug|x64.ActiveCfg = Debug|Any CPU + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Debug|x64.Build.0 = Debug|Any CPU + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Debug|x86.ActiveCfg = Debug|Any CPU + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Debug|x86.Build.0 = Debug|Any CPU + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Release|Any CPU.Build.0 = Release|Any CPU + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Release|x64.ActiveCfg = Release|Any CPU + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Release|x64.Build.0 = Release|Any CPU + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Release|x86.ActiveCfg = Release|Any CPU + {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Release|x86.Build.0 = Release|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|x64.ActiveCfg = Debug|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|x64.Build.0 = Debug|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|x86.ActiveCfg = Debug|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|x86.Build.0 = Debug|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|Any CPU.Build.0 = Release|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|x64.ActiveCfg = Release|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|x64.Build.0 = Release|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|x86.ActiveCfg = Release|Any CPU + {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Carter.HtmlNegotiator.csproj b/src/Carter.HtmlNegotiator.csproj deleted file mode 100644 index 9f5c4f4..0000000 --- a/src/Carter.HtmlNegotiator.csproj +++ /dev/null @@ -1,7 +0,0 @@ - - - - netstandard2.0 - - - diff --git a/src/Carter.HtmlNegotiator/Carter.HtmlNegotiator.csproj b/src/Carter.HtmlNegotiator/Carter.HtmlNegotiator.csproj new file mode 100644 index 0000000..711ae51 --- /dev/null +++ b/src/Carter.HtmlNegotiator/Carter.HtmlNegotiator.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + diff --git a/src/DefaultViewLocator.cs b/src/Carter.HtmlNegotiator/DefaultViewLocator.cs similarity index 100% rename from src/DefaultViewLocator.cs rename to src/Carter.HtmlNegotiator/DefaultViewLocator.cs diff --git a/src/HtmlNegotiator.cs b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs similarity index 100% rename from src/HtmlNegotiator.cs rename to src/Carter.HtmlNegotiator/HtmlNegotiator.cs diff --git a/src/IViewLocator.cs b/src/Carter.HtmlNegotiator/IViewLocator.cs similarity index 100% rename from src/IViewLocator.cs rename to src/Carter.HtmlNegotiator/IViewLocator.cs From 392988e5a834526b5e151555eae9bdff3264f34e Mon Sep 17 00:00:00 2001 From: Richard Tasker Date: Sun, 3 Jan 2021 16:17:52 +0000 Subject: [PATCH 02/11] Update Submodule, Add Sample Project --- Carter.HtmlNegotiator.sln | 40 +++++++++++++------ dependencies/Carter | 2 +- .../Carter.HtmlNegotiator.Sample.csproj | 12 ++++++ .../Carter.HtmlNegotiator.Sample/Program.cs | 26 ++++++++++++ .../Carter.HtmlNegotiator.Sample/Startup.cs | 31 ++++++++++++++ .../appsettings.Development.json | 9 +++++ .../appsettings.json | 10 +++++ 7 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 sample/Carter.HtmlNegotiator.Sample/Carter.HtmlNegotiator.Sample.csproj create mode 100644 sample/Carter.HtmlNegotiator.Sample/Program.cs create mode 100644 sample/Carter.HtmlNegotiator.Sample/Startup.cs create mode 100644 sample/Carter.HtmlNegotiator.Sample/appsettings.Development.json create mode 100644 sample/Carter.HtmlNegotiator.Sample/appsettings.json diff --git a/Carter.HtmlNegotiator.sln b/Carter.HtmlNegotiator.sln index 4a4ab16..e343bd1 100644 --- a/Carter.HtmlNegotiator.sln +++ b/Carter.HtmlNegotiator.sln @@ -5,7 +5,9 @@ VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carter.HtmlNegotiator", "src\Carter.HtmlNegotiator\Carter.HtmlNegotiator.csproj", "{486553EF-81DD-401C-AA24-A7A360DDC1F7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carter", "dependencies\Carter\src\Carter.csproj", "{12FAB6F8-C688-428B-9479-28F8C114A491}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carter.HtmlNegotiator.Sample", "sample\Carter.HtmlNegotiator.Sample\Carter.HtmlNegotiator.Sample.csproj", "{00A6A897-5C02-46E6-B333-8F605BE4145A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carter", "dependencies\Carter\src\Carter\Carter.csproj", "{8D717F79-9A5C-45FE-8B39-86A651563ADB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -32,17 +34,29 @@ Global {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Release|x64.Build.0 = Release|Any CPU {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Release|x86.ActiveCfg = Release|Any CPU {486553EF-81DD-401C-AA24-A7A360DDC1F7}.Release|x86.Build.0 = Release|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|Any CPU.Build.0 = Debug|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|x64.ActiveCfg = Debug|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|x64.Build.0 = Debug|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|x86.ActiveCfg = Debug|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Debug|x86.Build.0 = Debug|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|Any CPU.ActiveCfg = Release|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|Any CPU.Build.0 = Release|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|x64.ActiveCfg = Release|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|x64.Build.0 = Release|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|x86.ActiveCfg = Release|Any CPU - {12FAB6F8-C688-428B-9479-28F8C114A491}.Release|x86.Build.0 = Release|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Debug|x64.ActiveCfg = Debug|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Debug|x64.Build.0 = Debug|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Debug|x86.ActiveCfg = Debug|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Debug|x86.Build.0 = Debug|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Release|Any CPU.Build.0 = Release|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Release|x64.ActiveCfg = Release|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Release|x64.Build.0 = Release|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Release|x86.ActiveCfg = Release|Any CPU + {00A6A897-5C02-46E6-B333-8F605BE4145A}.Release|x86.Build.0 = Release|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Debug|x64.ActiveCfg = Debug|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Debug|x64.Build.0 = Debug|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Debug|x86.Build.0 = Debug|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Release|Any CPU.Build.0 = Release|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Release|x64.ActiveCfg = Release|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Release|x64.Build.0 = Release|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Release|x86.ActiveCfg = Release|Any CPU + {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/dependencies/Carter b/dependencies/Carter index 71ac6c5..3eff444 160000 --- a/dependencies/Carter +++ b/dependencies/Carter @@ -1 +1 @@ -Subproject commit 71ac6c595dba14057cb6a2dc8a94bd0b671a6ed7 +Subproject commit 3eff4447730f723f6c34688eb80414f55ba60af9 diff --git a/sample/Carter.HtmlNegotiator.Sample/Carter.HtmlNegotiator.Sample.csproj b/sample/Carter.HtmlNegotiator.Sample/Carter.HtmlNegotiator.Sample.csproj new file mode 100644 index 0000000..988aaf1 --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/Carter.HtmlNegotiator.Sample.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1 + + + + + + + + diff --git a/sample/Carter.HtmlNegotiator.Sample/Program.cs b/sample/Carter.HtmlNegotiator.Sample/Program.cs new file mode 100644 index 0000000..451a8e7 --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Carter.HtmlNegotiator.Sample +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/sample/Carter.HtmlNegotiator.Sample/Startup.cs b/sample/Carter.HtmlNegotiator.Sample/Startup.cs new file mode 100644 index 0000000..aa198ee --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/Startup.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Carter; + +namespace Carter.HtmlNegotiator.Sample +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddCarter(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(builder => builder.MapCarter()); + } + } +} diff --git a/sample/Carter.HtmlNegotiator.Sample/appsettings.Development.json b/sample/Carter.HtmlNegotiator.Sample/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/sample/Carter.HtmlNegotiator.Sample/appsettings.json b/sample/Carter.HtmlNegotiator.Sample/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} From 0fd6fd6678484250bf5dd59edc9a19b75f59d492 Mon Sep 17 00:00:00 2001 From: Richard Tasker Date: Sun, 3 Jan 2021 17:04:27 +0000 Subject: [PATCH 03/11] Change Namespaces, Change Carter Ref, Add ServiceCollectionExtensions.cs --- sample/Carter.HtmlNegotiator.Sample/Startup.cs | 2 +- .../Carter.HtmlNegotiator.csproj | 2 +- src/Carter.HtmlNegotiator/DefaultViewLocator.cs | 14 +++++++------- src/Carter.HtmlNegotiator/HtmlNegotiator.cs | 15 +++++++-------- src/Carter.HtmlNegotiator/IViewLocator.cs | 6 +++--- .../ServiceCollectionExtensions.cs | 15 +++++++++++++++ 6 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs diff --git a/sample/Carter.HtmlNegotiator.Sample/Startup.cs b/sample/Carter.HtmlNegotiator.Sample/Startup.cs index aa198ee..9f84a4e 100644 --- a/sample/Carter.HtmlNegotiator.Sample/Startup.cs +++ b/sample/Carter.HtmlNegotiator.Sample/Startup.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Carter; namespace Carter.HtmlNegotiator.Sample { @@ -13,6 +12,7 @@ public class Startup public void ConfigureServices(IServiceCollection services) { services.AddCarter(); + services.AddHtmlNegotiator(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Carter.HtmlNegotiator/Carter.HtmlNegotiator.csproj b/src/Carter.HtmlNegotiator/Carter.HtmlNegotiator.csproj index 711ae51..16f1f10 100644 --- a/src/Carter.HtmlNegotiator/Carter.HtmlNegotiator.csproj +++ b/src/Carter.HtmlNegotiator/Carter.HtmlNegotiator.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Carter.HtmlNegotiator/DefaultViewLocator.cs b/src/Carter.HtmlNegotiator/DefaultViewLocator.cs index a89e056..f507816 100644 --- a/src/Carter.HtmlNegotiator/DefaultViewLocator.cs +++ b/src/Carter.HtmlNegotiator/DefaultViewLocator.cs @@ -1,11 +1,11 @@ -namespace HtmlNegotiator -{ - using System; - using System.Collections.Generic; - using System.IO; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +namespace Carter.HtmlNegotiator +{ public class DefaultViewLocator : IViewLocator { private readonly IDictionary mappings; diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs index 89f46b1..e972759 100644 --- a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs +++ b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs @@ -1,12 +1,11 @@ -namespace HtmlNegotiator -{ - using System.Net; - using System.Threading; - using System.Threading.Tasks; - using Carter; - using HandlebarsDotNet; - using Microsoft.AspNetCore.Http; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using HandlebarsDotNet; +using Microsoft.AspNetCore.Http; +namespace Carter.HtmlNegotiator +{ public class HtmlNegotiator : IResponseNegotiator { private readonly IViewLocator viewLocator; diff --git a/src/Carter.HtmlNegotiator/IViewLocator.cs b/src/Carter.HtmlNegotiator/IViewLocator.cs index c72c9c9..affcaf1 100644 --- a/src/Carter.HtmlNegotiator/IViewLocator.cs +++ b/src/Carter.HtmlNegotiator/IViewLocator.cs @@ -1,7 +1,7 @@ -namespace HtmlNegotiator -{ - using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; +namespace Carter.HtmlNegotiator +{ public interface IViewLocator { string GetView(object model, HttpContext httpContext); diff --git a/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..a280b10 --- /dev/null +++ b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; + +namespace Carter.HtmlNegotiator +{ + public static class ServiceCollectionExtensions + { + public static void AddHtmlNegotiator(this IServiceCollection services) + { + services.AddScoped(p => new DefaultViewLocator(new Dictionary())); + services.AddScoped(); + } + } +} \ No newline at end of file From bd7cc0384e26f5123a7afe72730207cbf5a6aebf Mon Sep 17 00:00:00 2001 From: Richard Tasker Date: Wed, 6 Jan 2021 20:50:43 +0000 Subject: [PATCH 04/11] Initial Pass At Returning HTML - Strips back HtmlNegotiator - Adds HandlebarsViewEngine - Adds Tests and Sample Projects --- Carter.HtmlNegotiator.sln | 14 +++++ .../Features/Home/Echo.hbs | 9 +++ .../Features/Home/EchoViewModel.cs | 7 +++ .../Features/Home/HomeModule.cs | 21 +++++++ .../Features/Home/Index.hbs | 9 +++ .../Carter.HtmlNegotiator.Tests.csproj | 26 +++++++++ .../HtmlNegotiatorTests.cs | 57 +++++++++++++++++++ .../Stubs/StubViewEngine.cs | 10 ++++ .../Stubs/StubViewLocator.cs | 12 ++++ .../DefaultViewLocator.cs | 49 ++-------------- .../HandlebarsViewEngine.cs | 13 +++++ src/Carter.HtmlNegotiator/HtmlNegotiator.cs | 24 +++----- .../HttpResponseExtensions.cs | 13 +++++ src/Carter.HtmlNegotiator/IViewEngine.cs | 7 +++ src/Carter.HtmlNegotiator/IViewLocator.cs | 2 +- .../ServiceCollectionExtensions.cs | 3 +- 16 files changed, 216 insertions(+), 60 deletions(-) create mode 100644 sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs create mode 100644 sample/Carter.HtmlNegotiator.Sample/Features/Home/EchoViewModel.cs create mode 100644 sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs create mode 100644 sample/Carter.HtmlNegotiator.Sample/Features/Home/Index.hbs create mode 100644 src/Carter.HtmlNegotiator.Tests/Carter.HtmlNegotiator.Tests.csproj create mode 100644 src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs create mode 100644 src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs create mode 100644 src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs create mode 100644 src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs create mode 100644 src/Carter.HtmlNegotiator/HttpResponseExtensions.cs create mode 100644 src/Carter.HtmlNegotiator/IViewEngine.cs diff --git a/Carter.HtmlNegotiator.sln b/Carter.HtmlNegotiator.sln index e343bd1..7ff2a9c 100644 --- a/Carter.HtmlNegotiator.sln +++ b/Carter.HtmlNegotiator.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carter.HtmlNegotiator.Sampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carter", "dependencies\Carter\src\Carter\Carter.csproj", "{8D717F79-9A5C-45FE-8B39-86A651563ADB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carter.HtmlNegotiator.Tests", "src\Carter.HtmlNegotiator.Tests\Carter.HtmlNegotiator.Tests.csproj", "{A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,5 +60,17 @@ Global {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Release|x64.Build.0 = Release|Any CPU {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Release|x86.ActiveCfg = Release|Any CPU {8D717F79-9A5C-45FE-8B39-86A651563ADB}.Release|x86.Build.0 = Release|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Debug|x64.Build.0 = Debug|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Debug|x86.Build.0 = Debug|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Release|Any CPU.Build.0 = Release|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Release|x64.ActiveCfg = Release|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Release|x64.Build.0 = Release|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Release|x86.ActiveCfg = Release|Any CPU + {A6631AB5-2720-4DBC-A2F7-D00E8A70F9F0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs new file mode 100644 index 0000000..725ed0e --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs @@ -0,0 +1,9 @@ + + + + Echo Page + + +

Echo: {{Message}}

+ + \ No newline at end of file diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Home/EchoViewModel.cs b/sample/Carter.HtmlNegotiator.Sample/Features/Home/EchoViewModel.cs new file mode 100644 index 0000000..ec0ca1e --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/EchoViewModel.cs @@ -0,0 +1,7 @@ +namespace Carter.HtmlNegotiator.Sample.Features.Home +{ + public class EchoViewModel + { + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs b/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs new file mode 100644 index 0000000..c336621 --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs @@ -0,0 +1,21 @@ +using Carter.Request; +using Carter.Response; + +namespace Carter.HtmlNegotiator.Sample.Features.Home +{ + public class HomeModule : CarterModule + { + public HomeModule() + { + Get("/", (request, response) => response.Negotiate(new {})); + + Get("/{msg}", (request, response) => response + .WithView("Echo.hbs") + .Negotiate(new EchoViewModel + { + Message = request.RouteValues.As("msg") + }) + ); + } + } +} \ No newline at end of file diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Home/Index.hbs b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Index.hbs new file mode 100644 index 0000000..57516b2 --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Index.hbs @@ -0,0 +1,9 @@ + + + + Welcome Page + + +

Hello from Carter!

+ + \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Carter.HtmlNegotiator.Tests.csproj b/src/Carter.HtmlNegotiator.Tests/Carter.HtmlNegotiator.Tests.csproj new file mode 100644 index 0000000..8d1809a --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/Carter.HtmlNegotiator.Tests.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp3.1 + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs b/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs new file mode 100644 index 0000000..9f291f5 --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs @@ -0,0 +1,57 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Carter.HtmlNegotiator.Tests.Stubs; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Carter.HtmlNegotiator.Tests +{ + public class HtmlNegotiatorTests + { + private readonly HtmlNegotiator htmlNegotiator; + + public HtmlNegotiatorTests() + { + htmlNegotiator = new HtmlNegotiator(new StubViewLocator(), new StubViewEngine()); + } + + [Fact] + public void Should_Be_Able_To_Handle_Requests_With_A_HTML_MediaType() + { + var headerValue = new MediaTypeHeaderValue("text/html"); + + var result = this.htmlNegotiator.CanHandle(headerValue); + + Assert.True(result); + } + + [Fact] + public void Should_Not_Be_Able_To_Handle_Requests_Other_MediaTypes() + { + var headerValue = new MediaTypeHeaderValue("application/json"); + + var result = this.htmlNegotiator.CanHandle(headerValue); + + Assert.False(result); + } + + [Fact] + public async Task Should_Return_A_HTML_Response() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.Body = new MemoryStream(); + + await this.htmlNegotiator.Handle(httpContext.Request, httpContext.Response, "Hello from Carter!", CancellationToken.None); + + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal("text/html", httpContext.Response.ContentType); + + httpContext.Response.Body.Position = 0; + using var streamReader = new StreamReader(httpContext.Response.Body); + var actualResponseText = await streamReader.ReadToEndAsync(); + Assert.Contains("

Hello from Carter!

", actualResponseText); + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs new file mode 100644 index 0000000..7ab4a9d --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs @@ -0,0 +1,10 @@ +namespace Carter.HtmlNegotiator.Tests.Stubs +{ + public class StubViewEngine : IViewEngine + { + public string Compile(string source, object model) + { + return string.Format(source, model); + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs new file mode 100644 index 0000000..bd721cd --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; + +namespace Carter.HtmlNegotiator.Tests.Stubs +{ + public class StubViewLocator : IViewLocator + { + public string GetView(HttpContext httpContext) + { + return "

{0}

"; + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/DefaultViewLocator.cs b/src/Carter.HtmlNegotiator/DefaultViewLocator.cs index f507816..7da55e1 100644 --- a/src/Carter.HtmlNegotiator/DefaultViewLocator.cs +++ b/src/Carter.HtmlNegotiator/DefaultViewLocator.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -8,47 +6,12 @@ namespace Carter.HtmlNegotiator { public class DefaultViewLocator : IViewLocator { - private readonly IDictionary mappings; - - private readonly IDictionary htmlMappings; - - public DefaultViewLocator(IDictionary mappings) - { - this.mappings = mappings; - this.htmlMappings = new Dictionary(); - } - - public string GetView(object model, HttpContext httpContext) + public string GetView(HttpContext httpContext) { - string viewName = string.Empty; - try - { - viewName = this.mappings[model.GetType()]; - } - catch (Exception) - { - return string.Empty; - } - - if (this.htmlMappings.ContainsKey(model.GetType())) - { - return this.htmlMappings[model.GetType()]; - } - - var env = (IHostingEnvironment)httpContext.RequestServices.GetService(typeof(IHostingEnvironment)); - - try - { - var html = File.ReadAllText(Path.Combine(env.ContentRootPath, viewName)); - - this.htmlMappings.Add(model.GetType(), html); - - return html; - } - catch (FileNotFoundException) - { - return string.Empty; - } + var viewName = "Features/Home/Index.hbs"; + var env = httpContext.RequestServices.GetService(typeof(IWebHostEnvironment)) as IWebHostEnvironment; + return File.ReadAllText(Path.Combine(env.ContentRootPath, viewName)); + } } } diff --git a/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs new file mode 100644 index 0000000..9fac5ec --- /dev/null +++ b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs @@ -0,0 +1,13 @@ +using HandlebarsDotNet; + +namespace Carter.HtmlNegotiator +{ + public class HandlebarsViewEngine : IViewEngine + { + public string Compile(string source, object model) + { + var template = Handlebars.Compile(source); + return template(model); + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs index e972759..cb27e5e 100644 --- a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs +++ b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs @@ -1,41 +1,35 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -using HandlebarsDotNet; using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; namespace Carter.HtmlNegotiator { public class HtmlNegotiator : IResponseNegotiator { private readonly IViewLocator viewLocator; + private readonly IViewEngine viewEngine; - public HtmlNegotiator(IViewLocator viewLocator) + public HtmlNegotiator(IViewLocator viewLocator, IViewEngine viewEngine) { this.viewLocator = viewLocator; + this.viewEngine = viewEngine; } - public bool CanHandle(Microsoft.Net.Http.Headers.MediaTypeHeaderValue accept) + public bool CanHandle(MediaTypeHeaderValue accept) { return accept.MediaType.Equals("text/html"); } public async Task Handle(HttpRequest req, HttpResponse res, object model, CancellationToken cancellationToken) { - var source = viewLocator.GetView(model, res.HttpContext); - if (string.IsNullOrEmpty(source)) - { - res.StatusCode = 500; - res.ContentType = "text/plain"; - await res.WriteAsync("View not found", cancellationToken); - } - - var template = Handlebars.Compile(source); - + var template = viewLocator.GetView(req.HttpContext); + var html = viewEngine.Compile(template, model); + res.ContentType = "text/html"; res.StatusCode = (int)HttpStatusCode.OK; - - await res.WriteAsync(template(model), cancellationToken); + await res.WriteAsync(html, cancellationToken: cancellationToken); } } } diff --git a/src/Carter.HtmlNegotiator/HttpResponseExtensions.cs b/src/Carter.HtmlNegotiator/HttpResponseExtensions.cs new file mode 100644 index 0000000..bd48a5a --- /dev/null +++ b/src/Carter.HtmlNegotiator/HttpResponseExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Http; + +namespace Carter.HtmlNegotiator +{ + public static class HttpResponseExtensions + { + public static HttpResponse WithView(this HttpResponse response, string viewPath) + { + //response.HttpContext.Items.Add(""); + return response; + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/IViewEngine.cs b/src/Carter.HtmlNegotiator/IViewEngine.cs new file mode 100644 index 0000000..32eb882 --- /dev/null +++ b/src/Carter.HtmlNegotiator/IViewEngine.cs @@ -0,0 +1,7 @@ +namespace Carter.HtmlNegotiator +{ + public interface IViewEngine + { + string Compile(string source, object model); + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/IViewLocator.cs b/src/Carter.HtmlNegotiator/IViewLocator.cs index affcaf1..8724e77 100644 --- a/src/Carter.HtmlNegotiator/IViewLocator.cs +++ b/src/Carter.HtmlNegotiator/IViewLocator.cs @@ -4,6 +4,6 @@ namespace Carter.HtmlNegotiator { public interface IViewLocator { - string GetView(object model, HttpContext httpContext); + string GetView(HttpContext httpContext); } } diff --git a/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs index a280b10..10d6a4c 100644 --- a/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs +++ b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs @@ -8,7 +8,8 @@ public static class ServiceCollectionExtensions { public static void AddHtmlNegotiator(this IServiceCollection services) { - services.AddScoped(p => new DefaultViewLocator(new Dictionary())); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); } } From b45378683b4db2522d6586d7f8bad293e82c9b4b Mon Sep 17 00:00:00 2001 From: Richard Tasker Date: Sun, 10 Jan 2021 17:06:26 +0000 Subject: [PATCH 05/11] Return LocateViewResult.cs from ViewLocator.cs --- .../Carter.HtmlNegotiator.Tests.csproj | 1 + .../HtmlNegotiatorTests.cs | 32 +++++++++++++++---- .../Stubs/StubViewLocator.cs | 7 ++-- .../DefaultViewLocator.cs | 17 ---------- src/Carter.HtmlNegotiator/HtmlNegotiator.cs | 25 +++++++++++---- src/Carter.HtmlNegotiator/IViewLocator.cs | 2 +- src/Carter.HtmlNegotiator/LocateViewResult.cs | 26 +++++++++++++++ .../ServiceCollectionExtensions.cs | 2 +- src/Carter.HtmlNegotiator/ViewLocator.cs | 23 +++++++++++++ 9 files changed, 102 insertions(+), 33 deletions(-) delete mode 100644 src/Carter.HtmlNegotiator/DefaultViewLocator.cs create mode 100644 src/Carter.HtmlNegotiator/LocateViewResult.cs create mode 100644 src/Carter.HtmlNegotiator/ViewLocator.cs diff --git a/src/Carter.HtmlNegotiator.Tests/Carter.HtmlNegotiator.Tests.csproj b/src/Carter.HtmlNegotiator.Tests/Carter.HtmlNegotiator.Tests.csproj index 8d1809a..8551958 100644 --- a/src/Carter.HtmlNegotiator.Tests/Carter.HtmlNegotiator.Tests.csproj +++ b/src/Carter.HtmlNegotiator.Tests/Carter.HtmlNegotiator.Tests.csproj @@ -8,6 +8,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs b/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs index 9f291f5..fde481e 100644 --- a/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs +++ b/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs @@ -4,6 +4,7 @@ using Carter.HtmlNegotiator.Tests.Stubs; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; +using Shouldly; using Xunit; namespace Carter.HtmlNegotiator.Tests @@ -24,7 +25,7 @@ public void Should_Be_Able_To_Handle_Requests_With_A_HTML_MediaType() var result = this.htmlNegotiator.CanHandle(headerValue); - Assert.True(result); + result.ShouldBeTrue(); } [Fact] @@ -34,24 +35,43 @@ public void Should_Not_Be_Able_To_Handle_Requests_Other_MediaTypes() var result = this.htmlNegotiator.CanHandle(headerValue); - Assert.False(result); + result.ShouldBeFalse(); } [Fact] - public async Task Should_Return_A_HTML_Response() + public async Task Should_Return_A_HTML_Response_When_A_View_Has_Been_Found() { var httpContext = new DefaultHttpContext(); httpContext.Response.Body = new MemoryStream(); await this.htmlNegotiator.Handle(httpContext.Request, httpContext.Response, "Hello from Carter!", CancellationToken.None); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.Equal("text/html", httpContext.Response.ContentType); + httpContext.Response.StatusCode.ShouldBe(200); + httpContext.Response.ContentType.ShouldBe("text/html"); httpContext.Response.Body.Position = 0; using var streamReader = new StreamReader(httpContext.Response.Body); var actualResponseText = await streamReader.ReadToEndAsync(); - Assert.Contains("

Hello from Carter!

", actualResponseText); + actualResponseText.ShouldContain("

Hello from Carter!

"); + } + + [Fact] + public async Task Should_Return_A_Error_Response_When_A_View_Has_Not_Been_Found() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Path = "/not-found"; + httpContext.Response.Body = new MemoryStream(); + + await this.htmlNegotiator.Handle(httpContext.Request, httpContext.Response, "Hello from Carter!", CancellationToken.None); + + httpContext.Response.StatusCode.ShouldBe(500); + httpContext.Response.ContentType.ShouldBe("text/plain"); + + httpContext.Response.Body.Position = 0; + using var streamReader = new StreamReader(httpContext.Response.Body); + var actualResponseText = await streamReader.ReadToEndAsync(); + + actualResponseText.ShouldContain("The view 'Index.hbs' was not found."); } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs index bd721cd..6eef7ec 100644 --- a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs @@ -1,12 +1,15 @@ +using System.Collections.Generic; using Microsoft.AspNetCore.Http; namespace Carter.HtmlNegotiator.Tests.Stubs { public class StubViewLocator : IViewLocator { - public string GetView(HttpContext httpContext) + public LocateViewResult GetView(HttpContext httpContext, string viewName) { - return "

{0}

"; + return httpContext.Request.Path.HasValue + ? LocateViewResult.NotFound(viewName, new List()) + : LocateViewResult.Found(viewName, "

{0}

"); } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/DefaultViewLocator.cs b/src/Carter.HtmlNegotiator/DefaultViewLocator.cs deleted file mode 100644 index 7da55e1..0000000 --- a/src/Carter.HtmlNegotiator/DefaultViewLocator.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.IO; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; - -namespace Carter.HtmlNegotiator -{ - public class DefaultViewLocator : IViewLocator - { - public string GetView(HttpContext httpContext) - { - var viewName = "Features/Home/Index.hbs"; - var env = httpContext.RequestServices.GetService(typeof(IWebHostEnvironment)) as IWebHostEnvironment; - return File.ReadAllText(Path.Combine(env.ContentRootPath, viewName)); - - } - } -} diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs index cb27e5e..15d67fb 100644 --- a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs +++ b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs @@ -1,4 +1,5 @@ -using System.Net; +using System; +using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -11,6 +12,9 @@ public class HtmlNegotiator : IResponseNegotiator private readonly IViewLocator viewLocator; private readonly IViewEngine viewEngine; + private string notFoundError => @"The view '{0}' was not found. The following locations were searched: + {1}"; + public HtmlNegotiator(IViewLocator viewLocator, IViewEngine viewEngine) { this.viewLocator = viewLocator; @@ -24,12 +28,21 @@ public bool CanHandle(MediaTypeHeaderValue accept) public async Task Handle(HttpRequest req, HttpResponse res, object model, CancellationToken cancellationToken) { - var template = viewLocator.GetView(req.HttpContext); - var html = viewEngine.Compile(template, model); + var result = viewLocator.GetView(req.HttpContext, "Index.hbs"); - res.ContentType = "text/html"; - res.StatusCode = (int)HttpStatusCode.OK; - await res.WriteAsync(html, cancellationToken: cancellationToken); + if (result.Success) + { + var html = viewEngine.Compile(result.View, model); + res.ContentType = "text/html"; + res.StatusCode = (int)HttpStatusCode.OK; + await res.WriteAsync(html, cancellationToken: cancellationToken); + } + else + { + res.ContentType = "text/plain"; + res.StatusCode = (int)HttpStatusCode.InternalServerError; + await res.WriteAsync(string.Format(notFoundError, result.ViewName, string.Join(Environment.NewLine, result.SearchedLocations)), cancellationToken: cancellationToken); + } } } } diff --git a/src/Carter.HtmlNegotiator/IViewLocator.cs b/src/Carter.HtmlNegotiator/IViewLocator.cs index 8724e77..43cd626 100644 --- a/src/Carter.HtmlNegotiator/IViewLocator.cs +++ b/src/Carter.HtmlNegotiator/IViewLocator.cs @@ -4,6 +4,6 @@ namespace Carter.HtmlNegotiator { public interface IViewLocator { - string GetView(HttpContext httpContext); + LocateViewResult GetView(HttpContext httpContext, string viewName); } } diff --git a/src/Carter.HtmlNegotiator/LocateViewResult.cs b/src/Carter.HtmlNegotiator/LocateViewResult.cs new file mode 100644 index 0000000..023d481 --- /dev/null +++ b/src/Carter.HtmlNegotiator/LocateViewResult.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Carter.HtmlNegotiator +{ + public class LocateViewResult + { + private LocateViewResult(string viewName, string view, List searchedLocations) + { + ViewName = viewName; + View = view; + SearchedLocations = searchedLocations; + } + + public string View { get; } + + public string ViewName { get; } + + public List SearchedLocations { get; } + + public bool Success => !string.IsNullOrEmpty(View); + + public static LocateViewResult Found(string viewName, string view) => new LocateViewResult(viewName, view, null); + + public static LocateViewResult NotFound(string viewName, List searchedLocations) => new LocateViewResult(viewName, null, searchedLocations); + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs index 10d6a4c..c532f36 100644 --- a/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs +++ b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs @@ -8,7 +8,7 @@ public static class ServiceCollectionExtensions { public static void AddHtmlNegotiator(this IServiceCollection services) { - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); } diff --git a/src/Carter.HtmlNegotiator/ViewLocator.cs b/src/Carter.HtmlNegotiator/ViewLocator.cs new file mode 100644 index 0000000..ea7878b --- /dev/null +++ b/src/Carter.HtmlNegotiator/ViewLocator.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; + +namespace Carter.HtmlNegotiator +{ + public class ViewLocator : IViewLocator + { + private List mappings; + + public ViewLocator() + { + mappings = new List(); + } + public LocateViewResult GetView(HttpContext httpContext, string viewName) + { + var path = $"Features/Home/{viewName}"; + var env = httpContext.RequestServices.GetService(typeof(IWebHostEnvironment)) as IWebHostEnvironment; + return LocateViewResult.Found("Index", File.ReadAllText(Path.Combine(env.ContentRootPath, path))); + } + } +} From 33f3b3cafd0b3bf671176a4cfcb2fddd9931f690 Mon Sep 17 00:00:00 2001 From: Richard Tasker Date: Wed, 13 Jan 2021 17:24:52 +0000 Subject: [PATCH 06/11] Add ViewNameResolver.cs and ViewLocator.cs --- .../HtmlNegotiatorTests.cs | 5 +- .../Stubs/StubViewEngine.cs | 2 + .../ViewLocatorTests.cs | 33 ++++++++++++ .../ViewNameResolverTests.cs | 50 +++++++++++++++++++ src/Carter.HtmlNegotiator/Constants.cs | 7 +++ .../HandlebarsViewEngine.cs | 2 + src/Carter.HtmlNegotiator/HtmlNegotiator.cs | 10 +++- .../HtmlNegotiatorConfiguration.cs | 18 +++++++ src/Carter.HtmlNegotiator/IViewEngine.cs | 2 + src/Carter.HtmlNegotiator/ViewLocator.cs | 31 +++++++++--- src/Carter.HtmlNegotiator/ViewNameResolver.cs | 34 +++++++++++++ 11 files changed, 183 insertions(+), 11 deletions(-) create mode 100644 src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs create mode 100644 src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs create mode 100644 src/Carter.HtmlNegotiator/Constants.cs create mode 100644 src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs create mode 100644 src/Carter.HtmlNegotiator/ViewNameResolver.cs diff --git a/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs b/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs index fde481e..9a8be9e 100644 --- a/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs +++ b/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -15,7 +16,7 @@ public class HtmlNegotiatorTests public HtmlNegotiatorTests() { - htmlNegotiator = new HtmlNegotiator(new StubViewLocator(), new StubViewEngine()); + htmlNegotiator = new HtmlNegotiator(new ViewNameResolver(), new StubViewLocator(), new StubViewEngine(), new HtmlNegotiatorConfiguration(new List())); } [Fact] @@ -71,7 +72,7 @@ public async Task Should_Return_A_Error_Response_When_A_View_Has_Not_Been_Found( using var streamReader = new StreamReader(httpContext.Response.Body); var actualResponseText = await streamReader.ReadToEndAsync(); - actualResponseText.ShouldContain("The view 'Index.hbs' was not found."); + actualResponseText.ShouldContain("The view 'not-found.hbs' was not found."); } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs index 7ab4a9d..dbd3b2e 100644 --- a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs @@ -2,6 +2,8 @@ namespace Carter.HtmlNegotiator.Tests.Stubs { public class StubViewEngine : IViewEngine { + public string Extension => "hbs"; + public string Compile(string source, object model) { return string.Format(source, model); diff --git a/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs b/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs new file mode 100644 index 0000000..75b7a12 --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Shouldly; +using Xunit; + +namespace Carter.HtmlNegotiator.Tests +{ + public class ViewLocatorTests + { + [Fact] + public void Should_Throw_When_HttpContext_Is_Null() + { + var viewLocator = new ViewLocator(new HtmlNegotiatorConfiguration(new List())); + var ex = Should.Throw(() => viewLocator.GetView(null, null)); + ex.ParamName.ShouldBe("httpContext"); + } + + [Fact] + public void Should_Throw_When_ViewName_Is_Null_Or_Empty() + { + var viewLocator = new ViewLocator(new HtmlNegotiatorConfiguration(new List())); + var ex = Should.Throw(() => viewLocator.GetView(new DefaultHttpContext(), null)); + ex.ParamName.ShouldBe("viewName"); + } + + [Fact] + public void Should_Return_A_View_Located_Result() + { + + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs b/src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs new file mode 100644 index 0000000..155eada --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Http; +using Shouldly; +using Xunit; + +namespace Carter.HtmlNegotiator.Tests +{ + public class ViewNameResolverTests + { + [Fact] + public void Should_Return_Default_View_Name_With_Extension() + { + var subject = new ViewNameResolver(); + + var result = subject.Resolve(new DefaultHttpContext(), "Index", "hbs"); + + result.ShouldBe("Index.hbs"); + } + + [Fact] + public void Should_Return_View_Name_From_HTTP_Context_With_Extension() + { + var subject = new ViewNameResolver(); + + var context = new DefaultHttpContext(); + context.Items.Add(Constants.ViewNameKey, "my-view"); + + var result = subject.Resolve(context, "Index", "hbs"); + + result.ShouldBe("my-view.hbs"); + } + + [Theory] + [InlineData(null, "Index.hbs")] + [InlineData("", "Index.hbs")] + [InlineData("/", "Index.hbs")] + [InlineData("/about", "about.hbs")] + [InlineData("/orders/checkout", "checkout.hbs")] + public void Should_Return_View_Name_From_Request_Path_With_Extension(string path, string expected) + { + var subject = new ViewNameResolver(); + + var context = new DefaultHttpContext(); + context.Request.Path = path; + + var result = subject.Resolve(context, "Index", "hbs"); + + result.ShouldBe(expected); + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/Constants.cs b/src/Carter.HtmlNegotiator/Constants.cs new file mode 100644 index 0000000..557cf5a --- /dev/null +++ b/src/Carter.HtmlNegotiator/Constants.cs @@ -0,0 +1,7 @@ +namespace Carter.HtmlNegotiator +{ + public class Constants + { + public const string ViewNameKey = "View"; + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs index 9fac5ec..7477efd 100644 --- a/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs +++ b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs @@ -9,5 +9,7 @@ public string Compile(string source, object model) var template = Handlebars.Compile(source); return template(model); } + + public string Extension => "hbs"; } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs index 15d67fb..df0336e 100644 --- a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs +++ b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs @@ -9,16 +9,20 @@ namespace Carter.HtmlNegotiator { public class HtmlNegotiator : IResponseNegotiator { + private readonly ViewNameResolver viewNameResolver; private readonly IViewLocator viewLocator; private readonly IViewEngine viewEngine; + private readonly HtmlNegotiatorConfiguration configuration; private string notFoundError => @"The view '{0}' was not found. The following locations were searched: {1}"; - public HtmlNegotiator(IViewLocator viewLocator, IViewEngine viewEngine) + public HtmlNegotiator(ViewNameResolver viewNameResolver, IViewLocator viewLocator, IViewEngine viewEngine, HtmlNegotiatorConfiguration configuration) { + this.viewNameResolver = viewNameResolver; this.viewLocator = viewLocator; this.viewEngine = viewEngine; + this.configuration = configuration; } public bool CanHandle(MediaTypeHeaderValue accept) @@ -28,7 +32,9 @@ public bool CanHandle(MediaTypeHeaderValue accept) public async Task Handle(HttpRequest req, HttpResponse res, object model, CancellationToken cancellationToken) { - var result = viewLocator.GetView(req.HttpContext, "Index.hbs"); + var viewName = this.viewNameResolver.Resolve(req.HttpContext, configuration.DefaultViewName, viewEngine.Extension); + + var result = viewLocator.GetView(req.HttpContext, viewName); if (result.Success) { diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs b/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs new file mode 100644 index 0000000..9ea87e7 --- /dev/null +++ b/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Carter.HtmlNegotiator +{ + public class HtmlNegotiatorConfiguration + { + public HtmlNegotiatorConfiguration(List viewLocationConventions) + { + DefaultViewName = "Index"; + ViewLocationConventions = viewLocationConventions; + } + + public string DefaultViewName { get; } + + public List ViewLocationConventions { get; } + + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/IViewEngine.cs b/src/Carter.HtmlNegotiator/IViewEngine.cs index 32eb882..d2ec232 100644 --- a/src/Carter.HtmlNegotiator/IViewEngine.cs +++ b/src/Carter.HtmlNegotiator/IViewEngine.cs @@ -2,6 +2,8 @@ namespace Carter.HtmlNegotiator { public interface IViewEngine { + string Extension { get; } + string Compile(string source, object model); } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/ViewLocator.cs b/src/Carter.HtmlNegotiator/ViewLocator.cs index ea7878b..e864290 100644 --- a/src/Carter.HtmlNegotiator/ViewLocator.cs +++ b/src/Carter.HtmlNegotiator/ViewLocator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -7,17 +8,33 @@ namespace Carter.HtmlNegotiator { public class ViewLocator : IViewLocator { - private List mappings; + private readonly HtmlNegotiatorConfiguration configuration; - public ViewLocator() + public ViewLocator(HtmlNegotiatorConfiguration configuration) { - mappings = new List(); + this.configuration = configuration; } + public LocateViewResult GetView(HttpContext httpContext, string viewName) { - var path = $"Features/Home/{viewName}"; - var env = httpContext.RequestServices.GetService(typeof(IWebHostEnvironment)) as IWebHostEnvironment; - return LocateViewResult.Found("Index", File.ReadAllText(Path.Combine(env.ContentRootPath, path))); + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (string.IsNullOrEmpty(viewName)) + { + throw new ArgumentNullException(nameof(viewName)); + } + + foreach (var viewLocation in this.configuration.ViewLocationConventions) + { + var path = viewLocation; + var env = httpContext.RequestServices.GetService(typeof(IWebHostEnvironment)) as IWebHostEnvironment; + var allText = File.ReadAllText(Path.Combine(env.ContentRootPath, path)); + return LocateViewResult.Found(viewName, allText); + } + return LocateViewResult.NotFound(viewName, new List()); } } } diff --git a/src/Carter.HtmlNegotiator/ViewNameResolver.cs b/src/Carter.HtmlNegotiator/ViewNameResolver.cs new file mode 100644 index 0000000..53ed4c5 --- /dev/null +++ b/src/Carter.HtmlNegotiator/ViewNameResolver.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Linq; +using Microsoft.AspNetCore.Http; + +namespace Carter.HtmlNegotiator +{ + public class ViewNameResolver + { + public string Resolve(HttpContext context, string defaultViewName, string extension) + { + context.Items.TryGetValue(Constants.ViewNameKey, out var viewName); + + viewName ??= GetViewFromPath(context.Request.Path); + + viewName ??= defaultViewName; + + return $"{viewName}.{extension}"; + } + + private string GetViewFromPath(PathString path) + { + if (!path.HasValue || path.Value.All(x => x != '/')) + return null; + + var segments = path.Value.Split("/"); + + var viewName = segments.Last(); + return string.IsNullOrWhiteSpace(viewName) + ? null + : viewName; + } + } +} \ No newline at end of file From 5c86ed1ccbbccaf2795c8a58bd046330d9eb699b Mon Sep 17 00:00:00 2001 From: Richard Tasker Date: Wed, 13 Jan 2021 18:23:18 +0000 Subject: [PATCH 07/11] Clean Up --- .../Stubs/StubViewEngine.cs | 4 +-- .../Stubs/StubViewLocator.cs | 8 ++--- .../ViewLocatorTests.cs | 24 -------------- .../HandlebarsViewEngine.cs | 4 ++- src/Carter.HtmlNegotiator/HtmlNegotiator.cs | 12 +++---- src/Carter.HtmlNegotiator/IViewEngine.cs | 2 +- src/Carter.HtmlNegotiator/IViewLocator.cs | 5 +-- src/Carter.HtmlNegotiator/LocateViewResult.cs | 26 --------------- src/Carter.HtmlNegotiator/ViewLocator.cs | 33 ++----------------- 9 files changed, 21 insertions(+), 97 deletions(-) delete mode 100644 src/Carter.HtmlNegotiator/LocateViewResult.cs diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs index dbd3b2e..3269b26 100644 --- a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs @@ -4,9 +4,9 @@ public class StubViewEngine : IViewEngine { public string Extension => "hbs"; - public string Compile(string source, object model) + public string Compile(string viewLocation, object model) { - return string.Format(source, model); + return $"

{model}

"; } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs index 6eef7ec..3138e0f 100644 --- a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs @@ -5,11 +5,11 @@ namespace Carter.HtmlNegotiator.Tests.Stubs { public class StubViewLocator : IViewLocator { - public LocateViewResult GetView(HttpContext httpContext, string viewName) + public string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, string viewName) { - return httpContext.Request.Path.HasValue - ? LocateViewResult.NotFound(viewName, new List()) - : LocateViewResult.Found(viewName, "

{0}

"); + return viewName != "not-found.hbs" + ? "Views/Home/Index.hbs" + : null; } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs b/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs index 75b7a12..9cdd802 100644 --- a/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs +++ b/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs @@ -1,33 +1,9 @@ -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; -using Shouldly; using Xunit; namespace Carter.HtmlNegotiator.Tests { public class ViewLocatorTests { - [Fact] - public void Should_Throw_When_HttpContext_Is_Null() - { - var viewLocator = new ViewLocator(new HtmlNegotiatorConfiguration(new List())); - var ex = Should.Throw(() => viewLocator.GetView(null, null)); - ex.ParamName.ShouldBe("httpContext"); - } - [Fact] - public void Should_Throw_When_ViewName_Is_Null_Or_Empty() - { - var viewLocator = new ViewLocator(new HtmlNegotiatorConfiguration(new List())); - var ex = Should.Throw(() => viewLocator.GetView(new DefaultHttpContext(), null)); - ex.ParamName.ShouldBe("viewName"); - } - - [Fact] - public void Should_Return_A_View_Located_Result() - { - - } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs index 7477efd..0f276a2 100644 --- a/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs +++ b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs @@ -1,11 +1,13 @@ +using System.IO; using HandlebarsDotNet; namespace Carter.HtmlNegotiator { public class HandlebarsViewEngine : IViewEngine { - public string Compile(string source, object model) + public string Compile(string viewLocation, object model) { + var source = File.ReadAllText(viewLocation); var template = Handlebars.Compile(source); return template(model); } diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs index df0336e..1c53d53 100644 --- a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs +++ b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs @@ -1,5 +1,4 @@ -using System; -using System.Net; +using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -33,12 +32,11 @@ public bool CanHandle(MediaTypeHeaderValue accept) public async Task Handle(HttpRequest req, HttpResponse res, object model, CancellationToken cancellationToken) { var viewName = this.viewNameResolver.Resolve(req.HttpContext, configuration.DefaultViewName, viewEngine.Extension); - - var result = viewLocator.GetView(req.HttpContext, viewName); + var viewLocation = viewLocator.GetViewLocation(req.HttpContext, configuration.ViewLocationConventions, viewName); - if (result.Success) + if (!string.IsNullOrEmpty(viewLocation)) { - var html = viewEngine.Compile(result.View, model); + var html = viewEngine.Compile(viewLocation, model); res.ContentType = "text/html"; res.StatusCode = (int)HttpStatusCode.OK; await res.WriteAsync(html, cancellationToken: cancellationToken); @@ -47,7 +45,7 @@ public async Task Handle(HttpRequest req, HttpResponse res, object model, Cancel { res.ContentType = "text/plain"; res.StatusCode = (int)HttpStatusCode.InternalServerError; - await res.WriteAsync(string.Format(notFoundError, result.ViewName, string.Join(Environment.NewLine, result.SearchedLocations)), cancellationToken: cancellationToken); + await res.WriteAsync(string.Format(notFoundError, viewName, string.Empty), cancellationToken: cancellationToken); } } } diff --git a/src/Carter.HtmlNegotiator/IViewEngine.cs b/src/Carter.HtmlNegotiator/IViewEngine.cs index d2ec232..b7f7959 100644 --- a/src/Carter.HtmlNegotiator/IViewEngine.cs +++ b/src/Carter.HtmlNegotiator/IViewEngine.cs @@ -4,6 +4,6 @@ public interface IViewEngine { string Extension { get; } - string Compile(string source, object model); + string Compile(string viewLocation, object model); } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/IViewLocator.cs b/src/Carter.HtmlNegotiator/IViewLocator.cs index 43cd626..c97be0c 100644 --- a/src/Carter.HtmlNegotiator/IViewLocator.cs +++ b/src/Carter.HtmlNegotiator/IViewLocator.cs @@ -1,9 +1,10 @@ -using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; namespace Carter.HtmlNegotiator { public interface IViewLocator { - LocateViewResult GetView(HttpContext httpContext, string viewName); + string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, string viewName); } } diff --git a/src/Carter.HtmlNegotiator/LocateViewResult.cs b/src/Carter.HtmlNegotiator/LocateViewResult.cs deleted file mode 100644 index 023d481..0000000 --- a/src/Carter.HtmlNegotiator/LocateViewResult.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; - -namespace Carter.HtmlNegotiator -{ - public class LocateViewResult - { - private LocateViewResult(string viewName, string view, List searchedLocations) - { - ViewName = viewName; - View = view; - SearchedLocations = searchedLocations; - } - - public string View { get; } - - public string ViewName { get; } - - public List SearchedLocations { get; } - - public bool Success => !string.IsNullOrEmpty(View); - - public static LocateViewResult Found(string viewName, string view) => new LocateViewResult(viewName, view, null); - - public static LocateViewResult NotFound(string viewName, List searchedLocations) => new LocateViewResult(viewName, null, searchedLocations); - } -} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/ViewLocator.cs b/src/Carter.HtmlNegotiator/ViewLocator.cs index e864290..702d44d 100644 --- a/src/Carter.HtmlNegotiator/ViewLocator.cs +++ b/src/Carter.HtmlNegotiator/ViewLocator.cs @@ -1,40 +1,13 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.AspNetCore.Hosting; +using System.Collections.Generic; using Microsoft.AspNetCore.Http; namespace Carter.HtmlNegotiator { public class ViewLocator : IViewLocator { - private readonly HtmlNegotiatorConfiguration configuration; - - public ViewLocator(HtmlNegotiatorConfiguration configuration) - { - this.configuration = configuration; - } - - public LocateViewResult GetView(HttpContext httpContext, string viewName) + public string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, string viewName) { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - if (string.IsNullOrEmpty(viewName)) - { - throw new ArgumentNullException(nameof(viewName)); - } - - foreach (var viewLocation in this.configuration.ViewLocationConventions) - { - var path = viewLocation; - var env = httpContext.RequestServices.GetService(typeof(IWebHostEnvironment)) as IWebHostEnvironment; - var allText = File.ReadAllText(Path.Combine(env.ContentRootPath, path)); - return LocateViewResult.Found(viewName, allText); - } - return LocateViewResult.NotFound(viewName, new List()); + return string.Empty; } } } From ef3b78f27f36240c1f3b55e254db24ddc53f925d Mon Sep 17 00:00:00 2001 From: Richard Tasker Date: Thu, 14 Jan 2021 14:30:47 +0000 Subject: [PATCH 08/11] Fill Out The ViewLocator.cs Logic --- .../Stubs/StubFileSystem.cs | 19 +++ .../Stubs/StubViewLocator.cs | 3 +- .../ViewLocatorTests.cs | 109 ++++++++++++++++++ .../AmbiguousViewsException.cs | 17 +++ .../HandlebarsViewEngine.cs | 3 +- src/Carter.HtmlNegotiator/HtmlNegotiator.cs | 2 +- .../HtmlNegotiatorConfiguration.cs | 3 + src/Carter.HtmlNegotiator/IFileSystem.cs | 7 ++ src/Carter.HtmlNegotiator/IViewLocator.cs | 3 +- src/Carter.HtmlNegotiator/ViewLocator.cs | 57 ++++++++- .../ViewNotFoundException.cs | 17 +++ 11 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 src/Carter.HtmlNegotiator.Tests/Stubs/StubFileSystem.cs create mode 100644 src/Carter.HtmlNegotiator/AmbiguousViewsException.cs create mode 100644 src/Carter.HtmlNegotiator/IFileSystem.cs create mode 100644 src/Carter.HtmlNegotiator/ViewNotFoundException.cs diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubFileSystem.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubFileSystem.cs new file mode 100644 index 0000000..e902904 --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubFileSystem.cs @@ -0,0 +1,19 @@ +using System.Linq; + +namespace Carter.HtmlNegotiator.Tests.Stubs +{ + public class StubFileSystem : IFileSystem + { + private readonly string[] filePaths; + + public StubFileSystem(string[] filePaths) + { + this.filePaths = filePaths; + } + + public bool FileExists(string path) + { + return filePaths.Contains(path); + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs index 3138e0f..50454c4 100644 --- a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs @@ -5,7 +5,8 @@ namespace Carter.HtmlNegotiator.Tests.Stubs { public class StubViewLocator : IViewLocator { - public string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, string viewName) + public string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, + string rootResourceName, string viewName) { return viewName != "not-found.hbs" ? "Views/Home/Index.hbs" diff --git a/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs b/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs index 9cdd802..837c26f 100644 --- a/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs +++ b/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs @@ -1,9 +1,118 @@ +using System; +using Carter.HtmlNegotiator.Tests.Stubs; +using Microsoft.AspNetCore.Http; +using Shouldly; using Xunit; namespace Carter.HtmlNegotiator.Tests { public class ViewLocatorTests { + [Fact] + public void Should_Return_Null_When_View_Name_Is_Null() + { + var subject = new ViewLocator(new StubFileSystem(Array.Empty())); + + var result = subject.GetViewLocation(new DefaultHttpContext(), null, string.Empty, null); + + result.ShouldBeNull(); + } + + [Fact] + public void Should_Return_Null_When_View_Name_Is_Empty() + { + var subject = new ViewLocator(new StubFileSystem(Array.Empty())); + + var result = subject.GetViewLocation(new DefaultHttpContext(), null, string.Empty, string.Empty); + + result.ShouldBeNull(); + } + + [Fact] + public void Should_Use_View_Location_Conventions_To_Find_Views() + { + var fileSystem = new StubFileSystem(new []{ "Views/Index.hbs" }); + var subject = new ViewLocator(fileSystem); + + var result = subject.GetViewLocation(new DefaultHttpContext(), new []{ "Views/{view}" }, string.Empty, "Index.hbs"); + + result.ShouldNotBeNull(); + result.ShouldBe("Views/Index.hbs"); + } + + [Fact] + public void Should_Be_Able_To_Resolve_Default_Resource_Name() + { + var fileSystem = new StubFileSystem(new []{ "Views/Home/Index.hbs" }); + var subject = new ViewLocator(fileSystem); + + var result = subject.GetViewLocation(new DefaultHttpContext(), new []{ "Views/{resource}/{view}" }, "Home", "Index.hbs"); + + result.ShouldNotBeNull(); + result.ShouldBe("Views/Home/Index.hbs"); + } + + [Theory] + [InlineData("/Products", "Index.hbs", "Views/Products/Index.hbs")] + [InlineData("/Orders/Checkout", "Checkout.hbs", "Views/Orders/Checkout.hbs")] + public void Should_Be_Able_To_Resolve_Resource_Name_From_Request_Path(string requestPath, string viewName, string expected) + { + var fileSystem = new StubFileSystem(new [] + { + "Views/Products/Index.hbs", + "Views/Orders/Checkout.hbs" + }); + var subject = new ViewLocator(fileSystem); + + var context = new DefaultHttpContext(); + context.Request.Path = requestPath; + var result = subject.GetViewLocation(context, new []{ "Views/{resource}/{view}" }, "Home", viewName); + + result.ShouldNotBeNull(); + result.ShouldBe(expected); + } + [Fact] + public void Should_Be_Throw_An_AmbiguousViewsException_When_Multiple_View_Are_Located() + { + var fileSystem = new StubFileSystem(new [] + { + "Features/Products/Index.hbs", + "Views/Products/Index.hbs" + }); + var subject = new ViewLocator(fileSystem); + + var context = new DefaultHttpContext(); + context.Request.Path = "/Products"; + + var locationConventions = new [] + { + "Features/{resource}/{view}", + "Views/{resource}/{view}" + }; + + var ex = Should.Throw(() => subject.GetViewLocation(context, locationConventions, "Home", "Index.hbs")); + + ex.Message.ShouldContain("Views (2)"); + } + + [Fact] + public void Should_Be_Throw_An_ViewNotFoundException_When_No_Views_Are_Located() + { + var fileSystem = new StubFileSystem(new string[] {}); + var subject = new ViewLocator(fileSystem); + + var context = new DefaultHttpContext(); + context.Request.Path = "/Products"; + + var locationConventions = new [] + { + "Views/{resource}/{view}" + }; + + var ex = Should.Throw(() => subject.GetViewLocation(context, locationConventions, "Home", "Index.hbs")); + + ex.Message.ShouldContain("The view 'Index.hbs' was not found"); + } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/AmbiguousViewsException.cs b/src/Carter.HtmlNegotiator/AmbiguousViewsException.cs new file mode 100644 index 0000000..c08ae70 --- /dev/null +++ b/src/Carter.HtmlNegotiator/AmbiguousViewsException.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Carter.HtmlNegotiator +{ + public class AmbiguousViewsException : Exception + { + private static readonly string message = "Multiple views found. Views ({0})," + + Environment.NewLine + "{1}"; + + public AmbiguousViewsException(IEnumerable views) + : base(string.Format(message, views.Count(), string.Join(Environment.NewLine, views))) + { + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs index 0f276a2..eed2fd6 100644 --- a/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs +++ b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Text; using HandlebarsDotNet; namespace Carter.HtmlNegotiator @@ -7,7 +8,7 @@ public class HandlebarsViewEngine : IViewEngine { public string Compile(string viewLocation, object model) { - var source = File.ReadAllText(viewLocation); + var source = File.ReadAllText(viewLocation, Encoding.UTF8); var template = Handlebars.Compile(source); return template(model); } diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs index 1c53d53..6ad0859 100644 --- a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs +++ b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs @@ -32,7 +32,7 @@ public bool CanHandle(MediaTypeHeaderValue accept) public async Task Handle(HttpRequest req, HttpResponse res, object model, CancellationToken cancellationToken) { var viewName = this.viewNameResolver.Resolve(req.HttpContext, configuration.DefaultViewName, viewEngine.Extension); - var viewLocation = viewLocator.GetViewLocation(req.HttpContext, configuration.ViewLocationConventions, viewName); + var viewLocation = viewLocator.GetViewLocation(req.HttpContext, configuration.ViewLocationConventions, configuration.RootResourceName, viewName); if (!string.IsNullOrEmpty(viewLocation)) { diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs b/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs index 9ea87e7..568ad93 100644 --- a/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs +++ b/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs @@ -7,11 +7,14 @@ public class HtmlNegotiatorConfiguration public HtmlNegotiatorConfiguration(List viewLocationConventions) { DefaultViewName = "Index"; + RootResourceName = "Home"; ViewLocationConventions = viewLocationConventions; } public string DefaultViewName { get; } + public string RootResourceName { get; set; } + public List ViewLocationConventions { get; } } diff --git a/src/Carter.HtmlNegotiator/IFileSystem.cs b/src/Carter.HtmlNegotiator/IFileSystem.cs new file mode 100644 index 0000000..ff20324 --- /dev/null +++ b/src/Carter.HtmlNegotiator/IFileSystem.cs @@ -0,0 +1,7 @@ +namespace Carter.HtmlNegotiator +{ + public interface IFileSystem + { + bool FileExists(string path); + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/IViewLocator.cs b/src/Carter.HtmlNegotiator/IViewLocator.cs index c97be0c..1ca7fa7 100644 --- a/src/Carter.HtmlNegotiator/IViewLocator.cs +++ b/src/Carter.HtmlNegotiator/IViewLocator.cs @@ -5,6 +5,7 @@ namespace Carter.HtmlNegotiator { public interface IViewLocator { - string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, string viewName); + string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, + string rootResourceName, string viewName); } } diff --git a/src/Carter.HtmlNegotiator/ViewLocator.cs b/src/Carter.HtmlNegotiator/ViewLocator.cs index 702d44d..82dc30b 100644 --- a/src/Carter.HtmlNegotiator/ViewLocator.cs +++ b/src/Carter.HtmlNegotiator/ViewLocator.cs @@ -1,13 +1,66 @@ using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Http; namespace Carter.HtmlNegotiator { public class ViewLocator : IViewLocator { - public string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, string viewName) + private readonly IFileSystem fileSystem; + + public ViewLocator(IFileSystem fileSystem) + { + this.fileSystem = fileSystem; + } + + public string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, string rootResourceName, string viewName) { - return string.Empty; + if (string.IsNullOrEmpty(viewName)) + return null; + + var resource = GetResourceNameFromPath(httpContext.Request.Path); + resource ??= rootResourceName; + + var locatedViews = new List(); + var searchedLocations = new List(); + + foreach (var convention in locationConventions) + { + var path = convention + .Replace("{resource}", resource) + .Replace("{view}", viewName); + + if (this.fileSystem.FileExists(path)) + { + locatedViews.Add(path); + } + searchedLocations.Add(path); + } + + if (!locatedViews.Any()) + { + throw new ViewNotFoundException(viewName, searchedLocations); + } + + if (locatedViews.Count > 1) + { + throw new AmbiguousViewsException(locatedViews); + } + + return locatedViews.First(); + } + + private string GetResourceNameFromPath(PathString path) + { + if (!path.HasValue || path.Value.All(x => x != '/')) + return null; + + var segments = path.Value.Split("/"); + + var resource = segments[1]; + return string.IsNullOrWhiteSpace(resource) + ? null + : resource; } } } diff --git a/src/Carter.HtmlNegotiator/ViewNotFoundException.cs b/src/Carter.HtmlNegotiator/ViewNotFoundException.cs new file mode 100644 index 0000000..44e0bd9 --- /dev/null +++ b/src/Carter.HtmlNegotiator/ViewNotFoundException.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace Carter.HtmlNegotiator +{ + public class ViewNotFoundException : Exception + { + private static readonly string message = "The view '{0}' was not found. The following locations were searched:" + + Environment.NewLine + + "{1}"; + + public ViewNotFoundException(string viewName, IEnumerable locations) + : base(string.Format(message, viewName, string.Join(Environment.NewLine, locations))) + { + } + } +} \ No newline at end of file From d70cd40b8dd3e28023c99ce5089fdc5ce92e31c6 Mon Sep 17 00:00:00 2001 From: Richard Tasker Date: Thu, 14 Jan 2021 16:25:37 +0000 Subject: [PATCH 09/11] Wire-Up Service Collection Dependencies Resolve issues found in testing. --- .../Features/Home/HomeModule.cs | 4 ++-- src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs | 6 +++++- .../ViewNameResolverTests.cs | 13 +++++++++++++ src/Carter.HtmlNegotiator/FileSystem.cs | 12 ++++++++++++ .../HtmlNegotiatorConfiguration.cs | 6 +++--- src/Carter.HtmlNegotiator/HttpResponseExtensions.cs | 2 +- .../ServiceCollectionExtensions.cs | 8 ++++++++ src/Carter.HtmlNegotiator/ViewLocator.cs | 10 ++++++++-- src/Carter.HtmlNegotiator/ViewNameResolver.cs | 11 ++++++----- 9 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 src/Carter.HtmlNegotiator/FileSystem.cs diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs b/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs index c336621..3b06af9 100644 --- a/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs +++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs @@ -9,11 +9,11 @@ public HomeModule() { Get("/", (request, response) => response.Negotiate(new {})); - Get("/{msg}", (request, response) => response + Get("/echo", (request, response) => response .WithView("Echo.hbs") .Negotiate(new EchoViewModel { - Message = request.RouteValues.As("msg") + Message = request.Query.As("msg") }) ); } diff --git a/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs b/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs index 837c26f..f870166 100644 --- a/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs +++ b/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs @@ -55,17 +55,21 @@ public void Should_Be_Able_To_Resolve_Default_Resource_Name() [Theory] [InlineData("/Products", "Index.hbs", "Views/Products/Index.hbs")] [InlineData("/Orders/Checkout", "Checkout.hbs", "Views/Orders/Checkout.hbs")] + [InlineData("/Echo", "Echo.hbs", "Views/Home/Echo.hbs")] public void Should_Be_Able_To_Resolve_Resource_Name_From_Request_Path(string requestPath, string viewName, string expected) { var fileSystem = new StubFileSystem(new [] { + "Views/Home/Echo.hbs", "Views/Products/Index.hbs", "Views/Orders/Checkout.hbs" }); + var subject = new ViewLocator(fileSystem); - + var context = new DefaultHttpContext(); context.Request.Path = requestPath; + var result = subject.GetViewLocation(context, new []{ "Views/{resource}/{view}" }, "Home", viewName); result.ShouldNotBeNull(); diff --git a/src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs b/src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs index 155eada..132a5e2 100644 --- a/src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs +++ b/src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs @@ -29,6 +29,19 @@ public void Should_Return_View_Name_From_HTTP_Context_With_Extension() result.ShouldBe("my-view.hbs"); } + [Fact] + public void Should_Not_Append_Extension_When_Included_In_HttpContext() + { + var subject = new ViewNameResolver(); + + var context = new DefaultHttpContext(); + context.Items.Add(Constants.ViewNameKey, "my-view.hbs"); + + var result = subject.Resolve(context, "Index", "hbs"); + + result.ShouldBe("my-view.hbs"); + } + [Theory] [InlineData(null, "Index.hbs")] [InlineData("", "Index.hbs")] diff --git a/src/Carter.HtmlNegotiator/FileSystem.cs b/src/Carter.HtmlNegotiator/FileSystem.cs new file mode 100644 index 0000000..93d8cbf --- /dev/null +++ b/src/Carter.HtmlNegotiator/FileSystem.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace Carter.HtmlNegotiator +{ + public class FileSystem : IFileSystem + { + public bool FileExists(string path) + { + return File.Exists(path); + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs b/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs index 568ad93..8773958 100644 --- a/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs +++ b/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs @@ -4,7 +4,7 @@ namespace Carter.HtmlNegotiator { public class HtmlNegotiatorConfiguration { - public HtmlNegotiatorConfiguration(List viewLocationConventions) + public HtmlNegotiatorConfiguration(IEnumerable viewLocationConventions) { DefaultViewName = "Index"; RootResourceName = "Home"; @@ -13,9 +13,9 @@ public HtmlNegotiatorConfiguration(List viewLocationConventions) public string DefaultViewName { get; } - public string RootResourceName { get; set; } + public string RootResourceName { get; } - public List ViewLocationConventions { get; } + public IEnumerable ViewLocationConventions { get; } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/HttpResponseExtensions.cs b/src/Carter.HtmlNegotiator/HttpResponseExtensions.cs index bd48a5a..932975e 100644 --- a/src/Carter.HtmlNegotiator/HttpResponseExtensions.cs +++ b/src/Carter.HtmlNegotiator/HttpResponseExtensions.cs @@ -6,7 +6,7 @@ public static class HttpResponseExtensions { public static HttpResponse WithView(this HttpResponse response, string viewPath) { - //response.HttpContext.Items.Add(""); + response.HttpContext.Items.Add(Constants.ViewNameKey, viewPath); return response; } } diff --git a/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs index c532f36..d6358b9 100644 --- a/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs +++ b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs @@ -8,8 +8,16 @@ public static class ServiceCollectionExtensions { public static void AddHtmlNegotiator(this IServiceCollection services) { + services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(p => new HtmlNegotiatorConfiguration(new[] + { + "Views/{resource}/{view}", + "Features/{resource}/{view}", + "Features/{resource}/Views/{view}" + })); services.AddScoped(); } } diff --git a/src/Carter.HtmlNegotiator/ViewLocator.cs b/src/Carter.HtmlNegotiator/ViewLocator.cs index 82dc30b..495a3a1 100644 --- a/src/Carter.HtmlNegotiator/ViewLocator.cs +++ b/src/Carter.HtmlNegotiator/ViewLocator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Http; @@ -20,7 +21,12 @@ public string GetViewLocation(HttpContext httpContext, IEnumerable locat var resource = GetResourceNameFromPath(httpContext.Request.Path); resource ??= rootResourceName; - + + if (viewName.StartsWith(resource, StringComparison.InvariantCultureIgnoreCase)) + { + resource = rootResourceName; + } + var locatedViews = new List(); var searchedLocations = new List(); diff --git a/src/Carter.HtmlNegotiator/ViewNameResolver.cs b/src/Carter.HtmlNegotiator/ViewNameResolver.cs index 53ed4c5..090ded3 100644 --- a/src/Carter.HtmlNegotiator/ViewNameResolver.cs +++ b/src/Carter.HtmlNegotiator/ViewNameResolver.cs @@ -9,13 +9,14 @@ public class ViewNameResolver { public string Resolve(HttpContext context, string defaultViewName, string extension) { - context.Items.TryGetValue(Constants.ViewNameKey, out var viewName); + context.Items.TryGetValue(Constants.ViewNameKey, out var value); - viewName ??= GetViewFromPath(context.Request.Path); + value ??= GetViewFromPath(context.Request.Path) ?? defaultViewName; - viewName ??= defaultViewName; - - return $"{viewName}.{extension}"; + var viewName = value as string; + return viewName.EndsWith(extension) + ? viewName + : $"{viewName}.{extension}"; } private string GetViewFromPath(PathString path) From 0de57019f5e6111f81a122a250a7e3d75b863cbb Mon Sep 17 00:00:00 2001 From: Richard Tasker Date: Fri, 15 Jan 2021 13:44:42 +0000 Subject: [PATCH 10/11] Got Partials and Layouts Working --- .../Carter.HtmlNegotiator.Sample.csproj | 9 +++ .../Features/Home/Echo.hbs | 22 +++--- .../Features/Home/HomeModule.cs | 6 +- .../Features/Home/Index.hbs | 14 ++-- .../Shared/Layout/base.hbs | 50 +++++++++++++ .../Shared/Partials/footer.hbs | 3 + .../Shared/Partials/scripts.hbs | 2 + .../Carter.HtmlNegotiator.Sample/Startup.cs | 2 +- .../HandlebarsViewEngineTests.cs | 43 +++++++++++ .../HandlebarsViewEngine.cs | 72 ++++++++++++++++++- .../HtmlNegotiatorConfiguration.cs | 3 + .../ServiceCollectionExtensions.cs | 2 +- src/Carter.HtmlNegotiator/ViewNameResolver.cs | 2 - 13 files changed, 204 insertions(+), 26 deletions(-) create mode 100644 sample/Carter.HtmlNegotiator.Sample/Shared/Layout/base.hbs create mode 100644 sample/Carter.HtmlNegotiator.Sample/Shared/Partials/footer.hbs create mode 100644 sample/Carter.HtmlNegotiator.Sample/Shared/Partials/scripts.hbs create mode 100644 src/Carter.HtmlNegotiator.Tests/HandlebarsViewEngineTests.cs diff --git a/sample/Carter.HtmlNegotiator.Sample/Carter.HtmlNegotiator.Sample.csproj b/sample/Carter.HtmlNegotiator.Sample/Carter.HtmlNegotiator.Sample.csproj index 988aaf1..5cfab1b 100644 --- a/sample/Carter.HtmlNegotiator.Sample/Carter.HtmlNegotiator.Sample.csproj +++ b/sample/Carter.HtmlNegotiator.Sample/Carter.HtmlNegotiator.Sample.csproj @@ -9,4 +9,13 @@
+ + + + + + + + + diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs index 725ed0e..924972c 100644 --- a/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs +++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs @@ -1,9 +1,13 @@ - - - - Echo Page - - -

Echo: {{Message}}

- - \ No newline at end of file +{{#> layouts/base title="Echo Page" }} + {{#*inline "hero-block"}} +
+

Echo: {{Message}}

+
+
Light Linear
+
Dark Linear
+
Light Radial
+
Dark Radial
+
+
+ {{/inline}} +{{/layouts/base}} \ No newline at end of file diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs b/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs index 3b06af9..fab73ce 100644 --- a/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs +++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs @@ -7,7 +7,11 @@ public class HomeModule : CarterModule { public HomeModule() { - Get("/", (request, response) => response.Negotiate(new {})); + Get("/", (request, response) => response.Negotiate(new + { + Title = "Welcome To Carter", + Message = "Hello From Carter!" + })); Get("/echo", (request, response) => response .WithView("Echo.hbs") diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Home/Index.hbs b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Index.hbs index 57516b2..f2b089c 100644 --- a/sample/Carter.HtmlNegotiator.Sample/Features/Home/Index.hbs +++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Index.hbs @@ -1,9 +1,5 @@ - - - - Welcome Page - - -

Hello from Carter!

- - \ No newline at end of file +{{#*inline "content"}} +
+

{{Message}}

+
+{{/inline}} diff --git a/sample/Carter.HtmlNegotiator.Sample/Shared/Layout/base.hbs b/sample/Carter.HtmlNegotiator.Sample/Shared/Layout/base.hbs new file mode 100644 index 0000000..ad88531 --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/Shared/Layout/base.hbs @@ -0,0 +1,50 @@ + + + + + {{#if title}} + {{title}} + {{else}} + Base Page Title + {{/if}} + + + + + +
+ {{#> content}} + {{!-- Content goes here. --}} + {{/content}} +
+
+ {{> Partials/footer }} +
+ {{> Partials/scripts }} + + \ No newline at end of file diff --git a/sample/Carter.HtmlNegotiator.Sample/Shared/Partials/footer.hbs b/sample/Carter.HtmlNegotiator.Sample/Shared/Partials/footer.hbs new file mode 100644 index 0000000..c0841fe --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/Shared/Partials/footer.hbs @@ -0,0 +1,3 @@ +
+ Footer content here. +
\ No newline at end of file diff --git a/sample/Carter.HtmlNegotiator.Sample/Shared/Partials/scripts.hbs b/sample/Carter.HtmlNegotiator.Sample/Shared/Partials/scripts.hbs new file mode 100644 index 0000000..cf828e2 --- /dev/null +++ b/sample/Carter.HtmlNegotiator.Sample/Shared/Partials/scripts.hbs @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/sample/Carter.HtmlNegotiator.Sample/Startup.cs b/sample/Carter.HtmlNegotiator.Sample/Startup.cs index 9f84a4e..109bc64 100644 --- a/sample/Carter.HtmlNegotiator.Sample/Startup.cs +++ b/sample/Carter.HtmlNegotiator.Sample/Startup.cs @@ -23,8 +23,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseDeveloperExceptionPage(); } + app.UseStaticFiles(); app.UseRouting(); - app.UseEndpoints(builder => builder.MapCarter()); } } diff --git a/src/Carter.HtmlNegotiator.Tests/HandlebarsViewEngineTests.cs b/src/Carter.HtmlNegotiator.Tests/HandlebarsViewEngineTests.cs new file mode 100644 index 0000000..0e9ebf0 --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/HandlebarsViewEngineTests.cs @@ -0,0 +1,43 @@ +using HandlebarsDotNet; +using Xunit; + +namespace Carter.HtmlNegotiator.Tests +{ + public class HandlebarsViewEngineTests + { + [Fact] + public void BasicPartial() + { + var source = "{{#> layouts/base title=\"Welcome Page\" }}{{#*inline \"content\"}}

Hello from Carter!

{{/inline}}{{/layouts/base}}"; + + var handlebars = Handlebars.Create(new HandlebarsConfiguration + { + PartialTemplateResolver = new CustomPartialResolver() + }); + + + var template = handlebars.Compile(source); + + var data = new { + name = "Marc" + }; + + var result = template(data); + Assert.Equal("Hello, Marc!", result); + } + } + + public class CustomPartialResolver : IPartialTemplateResolver + { + public bool TryRegisterPartial(IHandlebars env, string partialName, string templatePath) + { + if (partialName == "person") + { + env.RegisterTemplate("person", "{{name}}"); + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs index eed2fd6..db82621 100644 --- a/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs +++ b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using HandlebarsDotNet; @@ -6,13 +8,77 @@ namespace Carter.HtmlNegotiator { public class HandlebarsViewEngine : IViewEngine { + private IHandlebars handlebars; + + public HandlebarsViewEngine() + { + handlebars = Handlebars.Create(new HandlebarsConfiguration + { + PartialTemplateResolver = new CustomPartialResolver() + }); + } + + public string Extension => "hbs"; + public string Compile(string viewLocation, object model) { - var source = File.ReadAllText(viewLocation, Encoding.UTF8); - var template = Handlebars.Compile(source); + var layout = GetLayout(); + var page = File.ReadAllText(viewLocation, Encoding.UTF8); + var template = handlebars.Compile(page + layout); return template(model); } - public string Extension => "hbs"; + private string GetLayout() + { + var layoutPath = "Shared/Layout"; + var files = new List(); + + if (Directory.Exists(layoutPath)) + { + files.AddRange(GetFiles(layoutPath, Extension)); + } + return File.ReadAllText(files.First().FullName); + } + + private static FileInfo[] GetFiles(string directoryPath, string desiredExtension) + { + var directoryInfo = new DirectoryInfo(directoryPath); + return directoryInfo.GetFiles($"*.{desiredExtension}"); + } + } + + public class CustomPartialResolver : IPartialTemplateResolver + { + private IEnumerable partialPaths = new[] + { + "Shared", + "Views/Shared" + }; + + public bool TryRegisterPartial(IHandlebars env, string partialName, string templatePath) + { + var partials = new List(); + + foreach (var path in partialPaths) + { + var combine = Path.Combine(path, $"{partialName}.hbs"); + if (File.Exists(combine)) + { + partials.Add(combine); + } + } + + if (!partials.Any()) + { + return false; + } + + foreach (var p in partials) + { + var template = File.ReadAllText(p, Encoding.UTF8); + env.RegisterTemplate(partialName, template); + } + return true; + } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs b/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs index 8773958..bb4951a 100644 --- a/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs +++ b/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs @@ -9,6 +9,7 @@ public HtmlNegotiatorConfiguration(IEnumerable viewLocationConventions) DefaultViewName = "Index"; RootResourceName = "Home"; ViewLocationConventions = viewLocationConventions; + PartialLocations = new[] {"Shared", "Views/Shared"}; } public string DefaultViewName { get; } @@ -17,5 +18,7 @@ public HtmlNegotiatorConfiguration(IEnumerable viewLocationConventions) public IEnumerable ViewLocationConventions { get; } + public IEnumerable PartialLocations { get; } + } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs index d6358b9..c9680cd 100644 --- a/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs +++ b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs @@ -11,13 +11,13 @@ public static void AddHtmlNegotiator(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(p => new HtmlNegotiatorConfiguration(new[] { "Views/{resource}/{view}", "Features/{resource}/{view}", "Features/{resource}/Views/{view}" })); + services.AddScoped(); services.AddScoped(); } } diff --git a/src/Carter.HtmlNegotiator/ViewNameResolver.cs b/src/Carter.HtmlNegotiator/ViewNameResolver.cs index 090ded3..f050e1a 100644 --- a/src/Carter.HtmlNegotiator/ViewNameResolver.cs +++ b/src/Carter.HtmlNegotiator/ViewNameResolver.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Linq; using Microsoft.AspNetCore.Http; From bb12c358a9469b0cf70d61f2d8e99d7529e2d3fd Mon Sep 17 00:00:00 2001 From: Richard Tasker Date: Tue, 23 Feb 2021 20:43:21 +0000 Subject: [PATCH 11/11] Refactor, Resolve Partials --- .../Features/Home/Echo.hbs | 18 +-- .../footer.hbs => Features/Shared/Footer.hbs} | 0 .../base.hbs => Features/Shared/Layout.hbs} | 8 +- .../Shared/Scripts.hbs} | 0 .../HandlebarsViewEngineTests.cs | 92 +++++++++---- .../HtmlNegotiatorTests.cs | 24 ++-- .../PartialTemplateResolverTests.cs | 37 ++++++ .../Stubs/StubFileSystem.cs | 18 ++- .../Stubs/StubHandlebarsEnvironment.cs | 95 ++++++++++++++ .../Stubs/StubPartialResolver.cs | 25 ++++ .../Stubs/StubViewEngine.cs | 18 ++- .../Stubs/StubViewLocator.cs | 16 --- .../Stubs/StubViewResolver.cs | 22 ++++ .../Stubs/StubWebHostEnvironment.cs | 21 +++ .../ViewLocatorTests.cs | 122 ------------------ .../ViewNameResolverTests.cs | 17 +-- .../ViewResolverTests.cs | 114 ++++++++++++++++ src/Carter.HtmlNegotiator/Constants.cs | 1 + src/Carter.HtmlNegotiator/FileSystem.cs | 11 ++ .../HandlebarsViewEngine.cs | 84 +++--------- src/Carter.HtmlNegotiator/HtmlNegotiator.cs | 27 ++-- .../HtmlNegotiatorConfiguration.cs | 9 +- src/Carter.HtmlNegotiator/IFileSystem.cs | 1 + src/Carter.HtmlNegotiator/IViewEngine.cs | 4 +- src/Carter.HtmlNegotiator/IViewLocator.cs | 11 -- src/Carter.HtmlNegotiator/IViewResolver.cs | 9 ++ .../PartialTemplateResolver.cs | 52 ++++++++ .../ServiceCollectionExtensions.cs | 25 +++- src/Carter.HtmlNegotiator/ViewNameResolver.cs | 11 +- .../{ViewLocator.cs => ViewResolver.cs} | 31 +++-- 30 files changed, 592 insertions(+), 331 deletions(-) rename sample/Carter.HtmlNegotiator.Sample/{Shared/Partials/footer.hbs => Features/Shared/Footer.hbs} (100%) rename sample/Carter.HtmlNegotiator.Sample/{Shared/Layout/base.hbs => Features/Shared/Layout.hbs} (88%) rename sample/Carter.HtmlNegotiator.Sample/{Shared/Partials/scripts.hbs => Features/Shared/Scripts.hbs} (100%) create mode 100644 src/Carter.HtmlNegotiator.Tests/PartialTemplateResolverTests.cs create mode 100644 src/Carter.HtmlNegotiator.Tests/Stubs/StubHandlebarsEnvironment.cs create mode 100644 src/Carter.HtmlNegotiator.Tests/Stubs/StubPartialResolver.cs delete mode 100644 src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs create mode 100644 src/Carter.HtmlNegotiator.Tests/Stubs/StubViewResolver.cs create mode 100644 src/Carter.HtmlNegotiator.Tests/Stubs/StubWebHostEnvironment.cs delete mode 100644 src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs create mode 100644 src/Carter.HtmlNegotiator.Tests/ViewResolverTests.cs delete mode 100644 src/Carter.HtmlNegotiator/IViewLocator.cs create mode 100644 src/Carter.HtmlNegotiator/IViewResolver.cs create mode 100644 src/Carter.HtmlNegotiator/PartialTemplateResolver.cs rename src/Carter.HtmlNegotiator/{ViewLocator.cs => ViewResolver.cs} (58%) diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs index 924972c..80366f0 100644 --- a/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs +++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs @@ -1,13 +1,5 @@ -{{#> layouts/base title="Echo Page" }} - {{#*inline "hero-block"}} -
-

Echo: {{Message}}

-
-
Light Linear
-
Dark Linear
-
Light Radial
-
Dark Radial
-
-
- {{/inline}} -{{/layouts/base}} \ No newline at end of file +{{#*inline "content"}} +
+

Echo: {{Message}}

+
+{{/inline}} \ No newline at end of file diff --git a/sample/Carter.HtmlNegotiator.Sample/Shared/Partials/footer.hbs b/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Footer.hbs similarity index 100% rename from sample/Carter.HtmlNegotiator.Sample/Shared/Partials/footer.hbs rename to sample/Carter.HtmlNegotiator.Sample/Features/Shared/Footer.hbs diff --git a/sample/Carter.HtmlNegotiator.Sample/Shared/Layout/base.hbs b/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Layout.hbs similarity index 88% rename from sample/Carter.HtmlNegotiator.Sample/Shared/Layout/base.hbs rename to sample/Carter.HtmlNegotiator.Sample/Features/Shared/Layout.hbs index ad88531..badf114 100644 --- a/sample/Carter.HtmlNegotiator.Sample/Shared/Layout/base.hbs +++ b/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Layout.hbs @@ -38,13 +38,11 @@
- {{#> content}} - {{!-- Content goes here. --}} - {{/content}} + {{> content}}
- {{> Partials/footer }} + {{> Footer }}
- {{> Partials/scripts }} + {{> Scripts }} \ No newline at end of file diff --git a/sample/Carter.HtmlNegotiator.Sample/Shared/Partials/scripts.hbs b/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Scripts.hbs similarity index 100% rename from sample/Carter.HtmlNegotiator.Sample/Shared/Partials/scripts.hbs rename to sample/Carter.HtmlNegotiator.Sample/Features/Shared/Scripts.hbs diff --git a/src/Carter.HtmlNegotiator.Tests/HandlebarsViewEngineTests.cs b/src/Carter.HtmlNegotiator.Tests/HandlebarsViewEngineTests.cs index 0e9ebf0..92a08fb 100644 --- a/src/Carter.HtmlNegotiator.Tests/HandlebarsViewEngineTests.cs +++ b/src/Carter.HtmlNegotiator.Tests/HandlebarsViewEngineTests.cs @@ -1,4 +1,7 @@ +using System.Collections.Generic; +using Carter.HtmlNegotiator.Tests.Stubs; using HandlebarsDotNet; +using Microsoft.AspNetCore.Http; using Xunit; namespace Carter.HtmlNegotiator.Tests @@ -6,38 +9,83 @@ namespace Carter.HtmlNegotiator.Tests public class HandlebarsViewEngineTests { [Fact] - public void BasicPartial() + public void Should_Return_Compiled_HTML() { - var source = "{{#> layouts/base title=\"Welcome Page\" }}{{#*inline \"content\"}}

Hello from Carter!

{{/inline}}{{/layouts/base}}"; - - var handlebars = Handlebars.Create(new HandlebarsConfiguration + var handlebars = Handlebars.Create(); + var viewResolver = new StubViewResolver(new Dictionary { - PartialTemplateResolver = new CustomPartialResolver() + ["Index.hbs"] = "

Hello from {{Name}}!

" }); + + var configuration = new HtmlNegotiatorConfiguration(new []{ "Views/{Resource}/{View}" }); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Path = "/"; + var subject = new HandlebarsViewEngine(handlebars, viewResolver, new ViewNameResolver(configuration), configuration); - var template = handlebars.Compile(source); + var result = subject.GetView(httpContext, new { Name = "Carter" }); - var data = new { - name = "Marc" - }; + Assert.Equal("

Hello from Carter!

", result); + } + + [Fact] + public void Should_Return_Compiled_HTML_Using_A_Layout() + { + var handlebars = Handlebars.Create(); + var viewLocator = new StubViewResolver(new Dictionary + { + ["Index.hbs"] = "{{#*inline \"content\"}}

Hello from Carter!

{{/inline}}", + ["Layout.hbs"] = "
{{> content}}
" + }); - var result = template(data); - Assert.Equal("Hello, Marc!", result); + var configuration = new HtmlNegotiatorConfiguration(new [] + { + "Views/{Resource}/{View}", + "Views/Shared/{View}" + }); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Path = "/"; + + var subject = new HandlebarsViewEngine(handlebars, viewLocator, new ViewNameResolver(configuration), configuration); + + var result = subject.GetView(httpContext, new { Name = "Carter" }); + + Assert.Equal("

Hello from Carter!

", result); } - } - - public class CustomPartialResolver : IPartialTemplateResolver - { - public bool TryRegisterPartial(IHandlebars env, string partialName, string templatePath) + + [Fact] + public void Should_Return_Compiled_HTML_Using_A_Layout_With_A_Partial() { - if (partialName == "person") + var handlebars = Handlebars.Create(new HandlebarsConfiguration { - env.RegisterTemplate("person", "{{name}}"); - return true; - } - - return false; + PartialTemplateResolver = new StubPartialResolver(new Dictionary + { + ["Footer"] = "
I'm A Footer
" + }) + }); + + var viewLocator = new StubViewResolver(new Dictionary + { + ["Index.hbs"] = "{{#*inline \"content\"}}

Hello from Carter!

{{/inline}}", + ["Layout.hbs"] = "
{{> content}}
{{> Footer}}" + }); + + var configuration = new HtmlNegotiatorConfiguration(new [] + { + "Views/{Resource}/{View}", + "Views/Shared/{View}" + }); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Path = "/"; + + var subject = new HandlebarsViewEngine(handlebars, viewLocator, new ViewNameResolver(configuration), configuration); + + var result = subject.GetView(httpContext, new { Name = "Carter" }); + + Assert.Equal("

Hello from Carter!

I'm A Footer
", result); } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs b/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs index 9a8be9e..2158694 100644 --- a/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs +++ b/src/Carter.HtmlNegotiator.Tests/HtmlNegotiatorTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -12,19 +11,13 @@ namespace Carter.HtmlNegotiator.Tests { public class HtmlNegotiatorTests { - private readonly HtmlNegotiator htmlNegotiator; - - public HtmlNegotiatorTests() - { - htmlNegotiator = new HtmlNegotiator(new ViewNameResolver(), new StubViewLocator(), new StubViewEngine(), new HtmlNegotiatorConfiguration(new List())); - } - [Fact] public void Should_Be_Able_To_Handle_Requests_With_A_HTML_MediaType() { var headerValue = new MediaTypeHeaderValue("text/html"); + var htmlNegotiator = new HtmlNegotiator(new StubViewEngine()); - var result = this.htmlNegotiator.CanHandle(headerValue); + var result = htmlNegotiator.CanHandle(headerValue); result.ShouldBeTrue(); } @@ -33,8 +26,9 @@ public void Should_Be_Able_To_Handle_Requests_With_A_HTML_MediaType() public void Should_Not_Be_Able_To_Handle_Requests_Other_MediaTypes() { var headerValue = new MediaTypeHeaderValue("application/json"); + var htmlNegotiator = new HtmlNegotiator(new StubViewEngine()); - var result = this.htmlNegotiator.CanHandle(headerValue); + var result = htmlNegotiator.CanHandle(headerValue); result.ShouldBeFalse(); } @@ -44,8 +38,9 @@ public async Task Should_Return_A_HTML_Response_When_A_View_Has_Been_Found() { var httpContext = new DefaultHttpContext(); httpContext.Response.Body = new MemoryStream(); + var htmlNegotiator = new HtmlNegotiator(new StubViewEngine()); - await this.htmlNegotiator.Handle(httpContext.Request, httpContext.Response, "Hello from Carter!", CancellationToken.None); + await htmlNegotiator.Handle(httpContext.Request, httpContext.Response, "Hello from Carter!", CancellationToken.None); httpContext.Response.StatusCode.ShouldBe(200); httpContext.Response.ContentType.ShouldBe("text/html"); @@ -57,15 +52,16 @@ public async Task Should_Return_A_HTML_Response_When_A_View_Has_Been_Found() } [Fact] - public async Task Should_Return_A_Error_Response_When_A_View_Has_Not_Been_Found() + public async Task Should_Return_A_404_Response_When_A_View_Has_Not_Been_Found() { var httpContext = new DefaultHttpContext(); httpContext.Request.Path = "/not-found"; httpContext.Response.Body = new MemoryStream(); + var htmlNegotiator = new HtmlNegotiator(new StubViewEngine(true)); - await this.htmlNegotiator.Handle(httpContext.Request, httpContext.Response, "Hello from Carter!", CancellationToken.None); + await htmlNegotiator.Handle(httpContext.Request, httpContext.Response, "Hello from Carter!", CancellationToken.None); - httpContext.Response.StatusCode.ShouldBe(500); + httpContext.Response.StatusCode.ShouldBe(404); httpContext.Response.ContentType.ShouldBe("text/plain"); httpContext.Response.Body.Position = 0; diff --git a/src/Carter.HtmlNegotiator.Tests/PartialTemplateResolverTests.cs b/src/Carter.HtmlNegotiator.Tests/PartialTemplateResolverTests.cs new file mode 100644 index 0000000..a7777a7 --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/PartialTemplateResolverTests.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Carter.HtmlNegotiator.Tests.Stubs; +using Microsoft.AspNetCore.Http; +using Shouldly; +using Xunit; + +namespace Carter.HtmlNegotiator.Tests +{ + public class PartialTemplateResolverTests + { + [Fact] + public void ShouldBeAbleToResolveSharedPartials() + { + var stubHandlebarsEnvironment = new StubHandlebarsEnvironment(); + var locationConventions = new [] + { + "Features/Shared/{View}" + }; + + var context = new DefaultHttpContext(); + context.Request.Path = "/"; + var httpContextAccessor = new HttpContextAccessor + { + HttpContext = context + }; + + var stubFileSystem = new StubFileSystem(new Dictionary{ ["Features/Shared/Footer.hbs"] = "
A Footer
" }); + var subject = new PartialTemplateResolver(new HtmlNegotiatorConfiguration(locationConventions),new StubWebHostEnvironment(), stubFileSystem); + + var result = subject.TryRegisterPartial(stubHandlebarsEnvironment, "Footer", null); + + result.ShouldBeTrue(); + stubHandlebarsEnvironment.Templates.ContainsKey("Footer").ShouldBeTrue(); + stubHandlebarsEnvironment.Templates["Footer"].ShouldBe("
A Footer
"); + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubFileSystem.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubFileSystem.cs index e902904..8bbc9d7 100644 --- a/src/Carter.HtmlNegotiator.Tests/Stubs/StubFileSystem.cs +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubFileSystem.cs @@ -1,19 +1,25 @@ -using System.Linq; +using System.Collections.Generic; namespace Carter.HtmlNegotiator.Tests.Stubs { public class StubFileSystem : IFileSystem { - private readonly string[] filePaths; - - public StubFileSystem(string[] filePaths) + private readonly IDictionary viewTemplates; + + public StubFileSystem(IDictionary viewTemplates) { - this.filePaths = filePaths; + this.viewTemplates = viewTemplates; } public bool FileExists(string path) { - return filePaths.Contains(path); + return viewTemplates.ContainsKey(path); + } + + public string ReadFileContents(string path) + { + viewTemplates.TryGetValue(path, out var source); + return source; } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubHandlebarsEnvironment.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubHandlebarsEnvironment.cs new file mode 100644 index 0000000..0356a99 --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubHandlebarsEnvironment.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.IO; +using HandlebarsDotNet; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Runtime; + +namespace Carter.HtmlNegotiator.Tests.Stubs +{ + public class StubHandlebarsEnvironment : IHandlebars + { + public Dictionary Templates { get; } + + public StubHandlebarsEnvironment() + { + Templates = new Dictionary(); + } + + public HandlebarsTemplate Compile(TextReader template) + { + throw new System.NotImplementedException(); + } + + public HandlebarsTemplate Compile(string template) + { + throw new System.NotImplementedException(); + } + + public HandlebarsTemplate CompileView(string templatePath) + { + throw new System.NotImplementedException(); + } + + public HandlebarsTemplate CompileView(string templatePath, ViewReaderFactory readerFactoryFactory) + { + throw new System.NotImplementedException(); + } + + public void RegisterTemplate(string templateName, HandlebarsTemplate template) + { + throw new System.NotImplementedException(); + } + + public void RegisterTemplate(string templateName, string template) + { + Templates.Add(templateName, template); + } + + public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) + { + throw new System.NotImplementedException(); + } + + public void RegisterHelper(string helperName, HandlebarsHelperWithOptions helperFunction) + { + throw new System.NotImplementedException(); + } + + public void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction) + { + throw new System.NotImplementedException(); + } + + public void RegisterHelper(string helperName, HandlebarsReturnWithOptionsHelper helperFunction) + { + throw new System.NotImplementedException(); + } + + public void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) + { + throw new System.NotImplementedException(); + } + + public void RegisterHelper(string helperName, HandlebarsReturnBlockHelper helperFunction) + { + throw new System.NotImplementedException(); + } + + public void RegisterHelper(IHelperDescriptor helperObject) + { + throw new System.NotImplementedException(); + } + + public void RegisterHelper(IHelperDescriptor helperObject) + { + throw new System.NotImplementedException(); + } + + public DisposableContainer Configure() + { + throw new System.NotImplementedException(); + } + + public HandlebarsConfiguration Configuration { get; } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubPartialResolver.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubPartialResolver.cs new file mode 100644 index 0000000..fd5f313 --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubPartialResolver.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using HandlebarsDotNet; + +namespace Carter.HtmlNegotiator.Tests.Stubs +{ + public class StubPartialResolver : IPartialTemplateResolver + { + private readonly Dictionary partials; + + public StubPartialResolver(Dictionary partials) + { + this.partials = partials; + } + + public bool TryRegisterPartial(IHandlebars env, string partialName, string templatePath) + { + if (partials.TryGetValue(partialName, out var partial)) + { + env.RegisterTemplate(partialName, partial); + return true; + } + return false; + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs index 3269b26..a6a5aaa 100644 --- a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewEngine.cs @@ -1,11 +1,27 @@ +using System; +using Microsoft.AspNetCore.Http; + namespace Carter.HtmlNegotiator.Tests.Stubs { public class StubViewEngine : IViewEngine { + private readonly bool viewNotFound; + + public StubViewEngine(bool viewNotFound = false) + { + this.viewNotFound = viewNotFound; + } + public string Extension => "hbs"; - public string Compile(string viewLocation, object model) + public string GetView(HttpContext httpContext, object model) { + if (viewNotFound) + { + var viewName = httpContext.Request.Path.ToString().TrimStart('/'); + throw new ViewNotFoundException($"{viewName}.hbs", Array.Empty()); + } + return $"

{model}

"; } } diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs deleted file mode 100644 index 50454c4..0000000 --- a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewLocator.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; - -namespace Carter.HtmlNegotiator.Tests.Stubs -{ - public class StubViewLocator : IViewLocator - { - public string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, - string rootResourceName, string viewName) - { - return viewName != "not-found.hbs" - ? "Views/Home/Index.hbs" - : null; - } - } -} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewResolver.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewResolver.cs new file mode 100644 index 0000000..25a4e0f --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubViewResolver.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace Carter.HtmlNegotiator.Tests.Stubs +{ + public class StubViewResolver : IViewResolver + { + private readonly Dictionary views; + + public StubViewResolver(Dictionary views) + { + this.views = views; + } + + public string GetView(HttpContext httpContext, string viewName) + { + return this.views.TryGetValue(viewName, out var view) + ? view + : null; + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/Stubs/StubWebHostEnvironment.cs b/src/Carter.HtmlNegotiator.Tests/Stubs/StubWebHostEnvironment.cs new file mode 100644 index 0000000..37cd337 --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/Stubs/StubWebHostEnvironment.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.FileProviders; + +namespace Carter.HtmlNegotiator.Tests.Stubs +{ + public class StubWebHostEnvironment : IWebHostEnvironment + { + public StubWebHostEnvironment() + { + ContentRootPath = String.Empty; + } + + public string ApplicationName { get; set; } + public IFileProvider ContentRootFileProvider { get; set; } + public string ContentRootPath { get; set; } + public string EnvironmentName { get; set; } + public IFileProvider WebRootFileProvider { get; set; } + public string WebRootPath { get; set; } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs b/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs deleted file mode 100644 index f870166..0000000 --- a/src/Carter.HtmlNegotiator.Tests/ViewLocatorTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using Carter.HtmlNegotiator.Tests.Stubs; -using Microsoft.AspNetCore.Http; -using Shouldly; -using Xunit; - -namespace Carter.HtmlNegotiator.Tests -{ - public class ViewLocatorTests - { - [Fact] - public void Should_Return_Null_When_View_Name_Is_Null() - { - var subject = new ViewLocator(new StubFileSystem(Array.Empty())); - - var result = subject.GetViewLocation(new DefaultHttpContext(), null, string.Empty, null); - - result.ShouldBeNull(); - } - - [Fact] - public void Should_Return_Null_When_View_Name_Is_Empty() - { - var subject = new ViewLocator(new StubFileSystem(Array.Empty())); - - var result = subject.GetViewLocation(new DefaultHttpContext(), null, string.Empty, string.Empty); - - result.ShouldBeNull(); - } - - [Fact] - public void Should_Use_View_Location_Conventions_To_Find_Views() - { - var fileSystem = new StubFileSystem(new []{ "Views/Index.hbs" }); - var subject = new ViewLocator(fileSystem); - - var result = subject.GetViewLocation(new DefaultHttpContext(), new []{ "Views/{view}" }, string.Empty, "Index.hbs"); - - result.ShouldNotBeNull(); - result.ShouldBe("Views/Index.hbs"); - } - - [Fact] - public void Should_Be_Able_To_Resolve_Default_Resource_Name() - { - var fileSystem = new StubFileSystem(new []{ "Views/Home/Index.hbs" }); - var subject = new ViewLocator(fileSystem); - - var result = subject.GetViewLocation(new DefaultHttpContext(), new []{ "Views/{resource}/{view}" }, "Home", "Index.hbs"); - - result.ShouldNotBeNull(); - result.ShouldBe("Views/Home/Index.hbs"); - } - - [Theory] - [InlineData("/Products", "Index.hbs", "Views/Products/Index.hbs")] - [InlineData("/Orders/Checkout", "Checkout.hbs", "Views/Orders/Checkout.hbs")] - [InlineData("/Echo", "Echo.hbs", "Views/Home/Echo.hbs")] - public void Should_Be_Able_To_Resolve_Resource_Name_From_Request_Path(string requestPath, string viewName, string expected) - { - var fileSystem = new StubFileSystem(new [] - { - "Views/Home/Echo.hbs", - "Views/Products/Index.hbs", - "Views/Orders/Checkout.hbs" - }); - - var subject = new ViewLocator(fileSystem); - - var context = new DefaultHttpContext(); - context.Request.Path = requestPath; - - var result = subject.GetViewLocation(context, new []{ "Views/{resource}/{view}" }, "Home", viewName); - - result.ShouldNotBeNull(); - result.ShouldBe(expected); - } - - [Fact] - public void Should_Be_Throw_An_AmbiguousViewsException_When_Multiple_View_Are_Located() - { - var fileSystem = new StubFileSystem(new [] - { - "Features/Products/Index.hbs", - "Views/Products/Index.hbs" - }); - var subject = new ViewLocator(fileSystem); - - var context = new DefaultHttpContext(); - context.Request.Path = "/Products"; - - var locationConventions = new [] - { - "Features/{resource}/{view}", - "Views/{resource}/{view}" - }; - - var ex = Should.Throw(() => subject.GetViewLocation(context, locationConventions, "Home", "Index.hbs")); - - ex.Message.ShouldContain("Views (2)"); - } - - [Fact] - public void Should_Be_Throw_An_ViewNotFoundException_When_No_Views_Are_Located() - { - var fileSystem = new StubFileSystem(new string[] {}); - var subject = new ViewLocator(fileSystem); - - var context = new DefaultHttpContext(); - context.Request.Path = "/Products"; - - var locationConventions = new [] - { - "Views/{resource}/{view}" - }; - - var ex = Should.Throw(() => subject.GetViewLocation(context, locationConventions, "Home", "Index.hbs")); - - ex.Message.ShouldContain("The view 'Index.hbs' was not found"); - } - } -} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs b/src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs index 132a5e2..a4a500b 100644 --- a/src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs +++ b/src/Carter.HtmlNegotiator.Tests/ViewNameResolverTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Shouldly; using Xunit; @@ -9,9 +10,9 @@ public class ViewNameResolverTests [Fact] public void Should_Return_Default_View_Name_With_Extension() { - var subject = new ViewNameResolver(); + var subject = new ViewNameResolver(new HtmlNegotiatorConfiguration(new List())); - var result = subject.Resolve(new DefaultHttpContext(), "Index", "hbs"); + var result = subject.Resolve(new DefaultHttpContext(), "hbs"); result.ShouldBe("Index.hbs"); } @@ -19,12 +20,12 @@ public void Should_Return_Default_View_Name_With_Extension() [Fact] public void Should_Return_View_Name_From_HTTP_Context_With_Extension() { - var subject = new ViewNameResolver(); + var subject = new ViewNameResolver(new HtmlNegotiatorConfiguration(new List())); var context = new DefaultHttpContext(); context.Items.Add(Constants.ViewNameKey, "my-view"); - var result = subject.Resolve(context, "Index", "hbs"); + var result = subject.Resolve(context, "hbs"); result.ShouldBe("my-view.hbs"); } @@ -32,12 +33,12 @@ public void Should_Return_View_Name_From_HTTP_Context_With_Extension() [Fact] public void Should_Not_Append_Extension_When_Included_In_HttpContext() { - var subject = new ViewNameResolver(); + var subject = new ViewNameResolver(new HtmlNegotiatorConfiguration(new List())); var context = new DefaultHttpContext(); context.Items.Add(Constants.ViewNameKey, "my-view.hbs"); - var result = subject.Resolve(context, "Index", "hbs"); + var result = subject.Resolve(context, "hbs"); result.ShouldBe("my-view.hbs"); } @@ -50,12 +51,12 @@ public void Should_Not_Append_Extension_When_Included_In_HttpContext() [InlineData("/orders/checkout", "checkout.hbs")] public void Should_Return_View_Name_From_Request_Path_With_Extension(string path, string expected) { - var subject = new ViewNameResolver(); + var subject = new ViewNameResolver(new HtmlNegotiatorConfiguration(new List())); var context = new DefaultHttpContext(); context.Request.Path = path; - var result = subject.Resolve(context, "Index", "hbs"); + var result = subject.Resolve(context, "hbs"); result.ShouldBe(expected); } diff --git a/src/Carter.HtmlNegotiator.Tests/ViewResolverTests.cs b/src/Carter.HtmlNegotiator.Tests/ViewResolverTests.cs new file mode 100644 index 0000000..afa344e --- /dev/null +++ b/src/Carter.HtmlNegotiator.Tests/ViewResolverTests.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using Carter.HtmlNegotiator.Tests.Stubs; +using Microsoft.AspNetCore.Http; +using Shouldly; +using Xunit; + +namespace Carter.HtmlNegotiator.Tests +{ + public class ViewResolverTests + { + [Fact] + public void Should_Return_Null_When_View_Name_Is_Null() + { + var subject = new ViewResolver(new StubFileSystem(new Dictionary()), new StubWebHostEnvironment(), new HtmlNegotiatorConfiguration(new List())); + + var result = subject.GetView(new DefaultHttpContext(), null); + + result.ShouldBeNull(); + } + + [Fact] + public void Should_Return_Null_When_View_Name_Is_Empty() + { + var subject = new ViewResolver(new StubFileSystem(new Dictionary()), new StubWebHostEnvironment(), new HtmlNegotiatorConfiguration(new List())); + + var result = subject.GetView(new DefaultHttpContext(), string.Empty); + + result.ShouldBeNull(); + } + + [Fact] + public void Should_Use_View_Location_Conventions_To_Resolve_View() + { + var fileSystem = new StubFileSystem(new Dictionary{ ["Views/Home/Index.hbs"] = "
Hello World!
" }); + var subject = new ViewResolver(fileSystem, new StubWebHostEnvironment(), new HtmlNegotiatorConfiguration(new[] { "Views/{Resource}/{View}" })); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Path = "/"; + + var result = subject.GetView(httpContext, "Index.hbs"); + + result.ShouldNotBeNull(); + result.ShouldBe("
Hello World!
"); + } + + [Theory] + [InlineData("/Products", "Index.hbs", "
Index
")] + [InlineData("/Orders/Checkout", "Checkout.hbs", "
Checkout
")] + [InlineData("/Echo", "Echo.hbs", "
Echo
")] + public void Should_Be_Able_To_Resolve_Resource_Name_From_Request_Path(string requestPath, string viewName, string expected) + { + var fileSystem = new StubFileSystem(new Dictionary + { + ["Views/Home/Echo.hbs"] = "
Echo
", + ["Views/Products/Index.hbs"] = "
Index
", + ["Views/Orders/Checkout.hbs"] = "
Checkout
" + }); + + var locationConventions = new [] + { + "Views/{Resource}/{View}" + }; + + var context = new DefaultHttpContext(); + context.Request.Path = requestPath; + + var subject = new ViewResolver(fileSystem, new StubWebHostEnvironment(), new HtmlNegotiatorConfiguration(locationConventions)); + + var result = subject.GetView(context, viewName); + + result.ShouldNotBeNull(); + result.ShouldBe(expected); + } + + [Fact] + public void Should_Be_Throw_An_AmbiguousViewsException_When_Multiple_View_Are_Found() + { + var fileSystem = new StubFileSystem(new Dictionary + { + ["Features/Products/Index.hbs"] = string.Empty, + ["Views/Products/Index.hbs"] = string.Empty + }); + + var locationConventions = new [] + { + "Features/{Resource}/{View}", + "Views/{Resource}/{View}" + }; + + var context = new DefaultHttpContext(); + context.Request.Path = "/Products"; + + var subject = new ViewResolver(fileSystem, new StubWebHostEnvironment(), new HtmlNegotiatorConfiguration(locationConventions)); + + var ex = Should.Throw(() => subject.GetView(context, "Index.hbs")); + + ex.Message.ShouldContain("Views (2)"); + } + + [Fact] + public void Should_Be_Throw_An_ViewNotFoundException_When_No_Views_Are_Found() + { + var fileSystem = new StubFileSystem(new Dictionary()); + var subject = new ViewResolver(fileSystem, new StubWebHostEnvironment(), new HtmlNegotiatorConfiguration(new List())); + + var context = new DefaultHttpContext(); + context.Request.Path = "/Products"; + + var ex = Should.Throw(() => subject.GetView(context, "Index.hbs")); + + ex.Message.ShouldContain("The view 'Index.hbs' was not found"); + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/Constants.cs b/src/Carter.HtmlNegotiator/Constants.cs index 557cf5a..b7e20c6 100644 --- a/src/Carter.HtmlNegotiator/Constants.cs +++ b/src/Carter.HtmlNegotiator/Constants.cs @@ -3,5 +3,6 @@ namespace Carter.HtmlNegotiator public class Constants { public const string ViewNameKey = "View"; + public const string ResourceNameKey = "Resource"; } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/FileSystem.cs b/src/Carter.HtmlNegotiator/FileSystem.cs index 93d8cbf..f2ff5d4 100644 --- a/src/Carter.HtmlNegotiator/FileSystem.cs +++ b/src/Carter.HtmlNegotiator/FileSystem.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Text; namespace Carter.HtmlNegotiator { @@ -8,5 +9,15 @@ public bool FileExists(string path) { return File.Exists(path); } + + public string ReadFileContents(string path) + { + return File.ReadAllText(path, Encoding.UTF8); + } + + public string[] GetFilesInDirectory(string path, string extension) + { + return Directory.GetFiles(path, $"*.{extension}"); + } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs index db82621..fc7ce59 100644 --- a/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs +++ b/src/Carter.HtmlNegotiator/HandlebarsViewEngine.cs @@ -1,84 +1,34 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; using HandlebarsDotNet; +using Microsoft.AspNetCore.Http; namespace Carter.HtmlNegotiator { public class HandlebarsViewEngine : IViewEngine { - private IHandlebars handlebars; + private readonly IHandlebars handlebars; + private readonly IViewResolver viewResolver; + private readonly ViewNameResolver viewNameResolver; + private readonly HtmlNegotiatorConfiguration configuration; + - public HandlebarsViewEngine() + public HandlebarsViewEngine(IHandlebars handlebars, IViewResolver viewResolver, + ViewNameResolver viewNameResolver, HtmlNegotiatorConfiguration configuration) { - handlebars = Handlebars.Create(new HandlebarsConfiguration - { - PartialTemplateResolver = new CustomPartialResolver() - }); + this.handlebars = handlebars; + this.viewResolver = viewResolver; + this.viewNameResolver = viewNameResolver; + this.configuration = configuration; } public string Extension => "hbs"; - public string Compile(string viewLocation, object model) - { - var layout = GetLayout(); - var page = File.ReadAllText(viewLocation, Encoding.UTF8); - var template = handlebars.Compile(page + layout); - return template(model); - } - - private string GetLayout() - { - var layoutPath = "Shared/Layout"; - var files = new List(); - - if (Directory.Exists(layoutPath)) - { - files.AddRange(GetFiles(layoutPath, Extension)); - } - return File.ReadAllText(files.First().FullName); - } - - private static FileInfo[] GetFiles(string directoryPath, string desiredExtension) + public string GetView(HttpContext httpContext, object model) { - var directoryInfo = new DirectoryInfo(directoryPath); - return directoryInfo.GetFiles($"*.{desiredExtension}"); - } - } - - public class CustomPartialResolver : IPartialTemplateResolver - { - private IEnumerable partialPaths = new[] - { - "Shared", - "Views/Shared" - }; - - public bool TryRegisterPartial(IHandlebars env, string partialName, string templatePath) - { - var partials = new List(); - - foreach (var path in partialPaths) - { - var combine = Path.Combine(path, $"{partialName}.hbs"); - if (File.Exists(combine)) - { - partials.Add(combine); - } - } - - if (!partials.Any()) - { - return false; - } + var layout = viewResolver.GetView(httpContext, $"{configuration.DefaultLayoutName}.{Extension}"); + var view = viewResolver.GetView(httpContext, viewNameResolver.Resolve(httpContext, Extension)); + var template = handlebars.Compile(string.Concat(view, layout)); - foreach (var p in partials) - { - var template = File.ReadAllText(p, Encoding.UTF8); - env.RegisterTemplate(partialName, template); - } - return true; + return template(model); } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs index 6ad0859..d617a9a 100644 --- a/src/Carter.HtmlNegotiator/HtmlNegotiator.cs +++ b/src/Carter.HtmlNegotiator/HtmlNegotiator.cs @@ -1,4 +1,5 @@ -using System.Net; +using System; +using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -8,20 +9,11 @@ namespace Carter.HtmlNegotiator { public class HtmlNegotiator : IResponseNegotiator { - private readonly ViewNameResolver viewNameResolver; - private readonly IViewLocator viewLocator; private readonly IViewEngine viewEngine; - private readonly HtmlNegotiatorConfiguration configuration; - private string notFoundError => @"The view '{0}' was not found. The following locations were searched: - {1}"; - - public HtmlNegotiator(ViewNameResolver viewNameResolver, IViewLocator viewLocator, IViewEngine viewEngine, HtmlNegotiatorConfiguration configuration) + public HtmlNegotiator(IViewEngine viewEngine) { - this.viewNameResolver = viewNameResolver; - this.viewLocator = viewLocator; this.viewEngine = viewEngine; - this.configuration = configuration; } public bool CanHandle(MediaTypeHeaderValue accept) @@ -31,21 +23,18 @@ public bool CanHandle(MediaTypeHeaderValue accept) public async Task Handle(HttpRequest req, HttpResponse res, object model, CancellationToken cancellationToken) { - var viewName = this.viewNameResolver.Resolve(req.HttpContext, configuration.DefaultViewName, viewEngine.Extension); - var viewLocation = viewLocator.GetViewLocation(req.HttpContext, configuration.ViewLocationConventions, configuration.RootResourceName, viewName); - - if (!string.IsNullOrEmpty(viewLocation)) + try { - var html = viewEngine.Compile(viewLocation, model); + var html = viewEngine.GetView(req.HttpContext, model); res.ContentType = "text/html"; res.StatusCode = (int)HttpStatusCode.OK; await res.WriteAsync(html, cancellationToken: cancellationToken); } - else + catch (ViewNotFoundException e) { res.ContentType = "text/plain"; - res.StatusCode = (int)HttpStatusCode.InternalServerError; - await res.WriteAsync(string.Format(notFoundError, viewName, string.Empty), cancellationToken: cancellationToken); + res.StatusCode = (int)HttpStatusCode.NotFound; + await res.WriteAsync(e.Message, cancellationToken: cancellationToken); } } } diff --git a/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs b/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs index bb4951a..15d94ac 100644 --- a/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs +++ b/src/Carter.HtmlNegotiator/HtmlNegotiatorConfiguration.cs @@ -8,17 +8,16 @@ public HtmlNegotiatorConfiguration(IEnumerable viewLocationConventions) { DefaultViewName = "Index"; RootResourceName = "Home"; + DefaultLayoutName = "Layout"; ViewLocationConventions = viewLocationConventions; - PartialLocations = new[] {"Shared", "Views/Shared"}; } - + public string DefaultViewName { get; } public string RootResourceName { get; } - public IEnumerable ViewLocationConventions { get; } - - public IEnumerable PartialLocations { get; } + public string DefaultLayoutName { get; } + public IEnumerable ViewLocationConventions { get; } } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/IFileSystem.cs b/src/Carter.HtmlNegotiator/IFileSystem.cs index ff20324..a835b97 100644 --- a/src/Carter.HtmlNegotiator/IFileSystem.cs +++ b/src/Carter.HtmlNegotiator/IFileSystem.cs @@ -3,5 +3,6 @@ namespace Carter.HtmlNegotiator public interface IFileSystem { bool FileExists(string path); + string ReadFileContents(string path); } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/IViewEngine.cs b/src/Carter.HtmlNegotiator/IViewEngine.cs index b7f7959..6fa202f 100644 --- a/src/Carter.HtmlNegotiator/IViewEngine.cs +++ b/src/Carter.HtmlNegotiator/IViewEngine.cs @@ -1,9 +1,11 @@ +using Microsoft.AspNetCore.Http; + namespace Carter.HtmlNegotiator { public interface IViewEngine { string Extension { get; } - string Compile(string viewLocation, object model); + string GetView(HttpContext httpContext, object model); } } \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/IViewLocator.cs b/src/Carter.HtmlNegotiator/IViewLocator.cs deleted file mode 100644 index 1ca7fa7..0000000 --- a/src/Carter.HtmlNegotiator/IViewLocator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; - -namespace Carter.HtmlNegotiator -{ - public interface IViewLocator - { - string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, - string rootResourceName, string viewName); - } -} diff --git a/src/Carter.HtmlNegotiator/IViewResolver.cs b/src/Carter.HtmlNegotiator/IViewResolver.cs new file mode 100644 index 0000000..bdb8650 --- /dev/null +++ b/src/Carter.HtmlNegotiator/IViewResolver.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Http; + +namespace Carter.HtmlNegotiator +{ + public interface IViewResolver + { + string GetView(HttpContext httpContext, string viewName); + } +} diff --git a/src/Carter.HtmlNegotiator/PartialTemplateResolver.cs b/src/Carter.HtmlNegotiator/PartialTemplateResolver.cs new file mode 100644 index 0000000..eaf3341 --- /dev/null +++ b/src/Carter.HtmlNegotiator/PartialTemplateResolver.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using HandlebarsDotNet; +using Microsoft.AspNetCore.Hosting; + +namespace Carter.HtmlNegotiator +{ + public class PartialTemplateResolver : IPartialTemplateResolver + { + private readonly HtmlNegotiatorConfiguration configuration; + private readonly IWebHostEnvironment environment; + private readonly IFileSystem fileSystem; + + public PartialTemplateResolver(HtmlNegotiatorConfiguration configuration, IWebHostEnvironment environment, IFileSystem fileSystem) + { + this.configuration = configuration; + this.environment = environment; + this.fileSystem = fileSystem; + } + + public bool TryRegisterPartial(IHandlebars env, string partialName, string templatePath) + { + var locatedTemplates = new List(); + + foreach (var convention in configuration.ViewLocationConventions) + { + var path = convention + .Replace($"{{{Constants.ViewNameKey}}}", $"{partialName}.hbs"); + + var fullPath = Path.Combine(environment.ContentRootPath, path); + if (this.fileSystem.FileExists(fullPath)) + { + locatedTemplates.Add(fullPath); + } + } + + if (!locatedTemplates.Any()) + { + return false; + } + + if (locatedTemplates.Count > 1) + { + throw new AmbiguousViewsException(locatedTemplates); + } + + env.RegisterTemplate(partialName, fileSystem.ReadFileContents(locatedTemplates.First())); + return true; + } + } +} \ No newline at end of file diff --git a/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs index c9680cd..dfb9b13 100644 --- a/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs +++ b/src/Carter.HtmlNegotiator/ServiceCollectionExtensions.cs @@ -1,5 +1,5 @@ -using System; -using System.Collections.Generic; +using HandlebarsDotNet; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; namespace Carter.HtmlNegotiator @@ -8,15 +8,26 @@ public static class ServiceCollectionExtensions { public static void AddHtmlNegotiator(this IServiceCollection services) { + services.AddScoped(p => new HtmlNegotiatorConfiguration(new[] + { + "Views/{View}", + "Views/Shared/{View}", + "Views/{Resource}/{View}", + "Features/{View}", + "Features/Shared/{View}", + "Features/{Resource}/{View}" + })); + services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(p => new HtmlNegotiatorConfiguration(new[] + services.AddScoped(); + + services.AddScoped(p => Handlebars.Create(new HandlebarsConfiguration { - "Views/{resource}/{view}", - "Features/{resource}/{view}", - "Features/{resource}/Views/{view}" + PartialTemplateResolver = p.GetService() })); + services.AddScoped(); services.AddScoped(); } diff --git a/src/Carter.HtmlNegotiator/ViewNameResolver.cs b/src/Carter.HtmlNegotiator/ViewNameResolver.cs index f050e1a..4dcb47c 100644 --- a/src/Carter.HtmlNegotiator/ViewNameResolver.cs +++ b/src/Carter.HtmlNegotiator/ViewNameResolver.cs @@ -5,11 +5,18 @@ namespace Carter.HtmlNegotiator { public class ViewNameResolver { - public string Resolve(HttpContext context, string defaultViewName, string extension) + private readonly HtmlNegotiatorConfiguration configuration; + + public ViewNameResolver(HtmlNegotiatorConfiguration configuration) + { + this.configuration = configuration; + } + + public string Resolve(HttpContext context, string extension) { context.Items.TryGetValue(Constants.ViewNameKey, out var value); - value ??= GetViewFromPath(context.Request.Path) ?? defaultViewName; + value ??= GetViewFromPath(context.Request.Path) ?? configuration.DefaultViewName; var viewName = value as string; return viewName.EndsWith(extension) diff --git a/src/Carter.HtmlNegotiator/ViewLocator.cs b/src/Carter.HtmlNegotiator/ViewResolver.cs similarity index 58% rename from src/Carter.HtmlNegotiator/ViewLocator.cs rename to src/Carter.HtmlNegotiator/ViewResolver.cs index 495a3a1..6b9cc65 100644 --- a/src/Carter.HtmlNegotiator/ViewLocator.cs +++ b/src/Carter.HtmlNegotiator/ViewResolver.cs @@ -1,44 +1,51 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; namespace Carter.HtmlNegotiator { - public class ViewLocator : IViewLocator + public class ViewResolver : IViewResolver { private readonly IFileSystem fileSystem; + private readonly IWebHostEnvironment env; + private readonly HtmlNegotiatorConfiguration configuration; - public ViewLocator(IFileSystem fileSystem) + public ViewResolver(IFileSystem fileSystem, IWebHostEnvironment env, HtmlNegotiatorConfiguration configuration) { this.fileSystem = fileSystem; + this.env = env; + this.configuration = configuration; } - public string GetViewLocation(HttpContext httpContext, IEnumerable locationConventions, string rootResourceName, string viewName) + public string GetView(HttpContext httpContext, string viewName) { if (string.IsNullOrEmpty(viewName)) return null; var resource = GetResourceNameFromPath(httpContext.Request.Path); - resource ??= rootResourceName; + resource ??= configuration.RootResourceName; if (viewName.StartsWith(resource, StringComparison.InvariantCultureIgnoreCase)) { - resource = rootResourceName; + resource = configuration.RootResourceName; } var locatedViews = new List(); var searchedLocations = new List(); - foreach (var convention in locationConventions) + foreach (var convention in configuration.ViewLocationConventions) { var path = convention - .Replace("{resource}", resource) - .Replace("{view}", viewName); - - if (this.fileSystem.FileExists(path)) + .Replace($"{{{Constants.ResourceNameKey}}}", resource) + .Replace($"{{{Constants.ViewNameKey}}}", viewName); + + var fullPath = Path.Combine(env.ContentRootPath, path); + if (this.fileSystem.FileExists(fullPath)) { - locatedViews.Add(path); + locatedViews.Add(fullPath); } searchedLocations.Add(path); } @@ -53,7 +60,7 @@ public string GetViewLocation(HttpContext httpContext, IEnumerable locat throw new AmbiguousViewsException(locatedViews); } - return locatedViews.First(); + return fileSystem.ReadFileContents(locatedViews.First()); } private string GetResourceNameFromPath(PathString path)