diff --git a/Carter.HtmlNegotiator.sln b/Carter.HtmlNegotiator.sln
new file mode 100644
index 0000000..7ff2a9c
--- /dev/null
+++ b/Carter.HtmlNegotiator.sln
@@ -0,0 +1,76 @@
+
+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.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
+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
+ 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
+ {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
+ {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/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..5cfab1b
--- /dev/null
+++ b/sample/Carter.HtmlNegotiator.Sample/Carter.HtmlNegotiator.Sample.csproj
@@ -0,0 +1,21 @@
+
+
+
+ netcoreapp3.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..80366f0
--- /dev/null
+++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Echo.hbs
@@ -0,0 +1,5 @@
+{{#*inline "content"}}
+
+
Echo: {{Message}}
+
+{{/inline}}
\ 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..fab73ce
--- /dev/null
+++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/HomeModule.cs
@@ -0,0 +1,25 @@
+using Carter.Request;
+using Carter.Response;
+
+namespace Carter.HtmlNegotiator.Sample.Features.Home
+{
+ public class HomeModule : CarterModule
+ {
+ public HomeModule()
+ {
+ Get("/", (request, response) => response.Negotiate(new
+ {
+ Title = "Welcome To Carter",
+ Message = "Hello From Carter!"
+ }));
+
+ Get("/echo", (request, response) => response
+ .WithView("Echo.hbs")
+ .Negotiate(new EchoViewModel
+ {
+ Message = request.Query.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..f2b089c
--- /dev/null
+++ b/sample/Carter.HtmlNegotiator.Sample/Features/Home/Index.hbs
@@ -0,0 +1,5 @@
+{{#*inline "content"}}
+
+
{{Message}}
+
+{{/inline}}
diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Footer.hbs b/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Footer.hbs
new file mode 100644
index 0000000..c0841fe
--- /dev/null
+++ b/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Footer.hbs
@@ -0,0 +1,3 @@
+
+ Footer content here.
+
\ No newline at end of file
diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Layout.hbs b/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Layout.hbs
new file mode 100644
index 0000000..badf114
--- /dev/null
+++ b/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Layout.hbs
@@ -0,0 +1,48 @@
+
+
+
+
+ {{#if title}}
+ {{title}}
+ {{else}}
+ Base Page Title
+ {{/if}}
+
+
+
+
+
+
+ {{> content}}
+
+
+ {{> Scripts }}
+
+
\ No newline at end of file
diff --git a/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Scripts.hbs b/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Scripts.hbs
new file mode 100644
index 0000000..cf828e2
--- /dev/null
+++ b/sample/Carter.HtmlNegotiator.Sample/Features/Shared/Scripts.hbs
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
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..109bc64
--- /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;
+
+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();
+ services.AddHtmlNegotiator();
+ }
+
+ // 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.UseStaticFiles();
+ 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": "*"
+}
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..8551958
--- /dev/null
+++ b/src/Carter.HtmlNegotiator.Tests/Carter.HtmlNegotiator.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ netcoreapp3.1
+
+ false
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/src/Carter.HtmlNegotiator.Tests/HandlebarsViewEngineTests.cs b/src/Carter.HtmlNegotiator.Tests/HandlebarsViewEngineTests.cs
new file mode 100644
index 0000000..92a08fb
--- /dev/null
+++ b/src/Carter.HtmlNegotiator.Tests/HandlebarsViewEngineTests.cs
@@ -0,0 +1,91 @@
+using System.Collections.Generic;
+using Carter.HtmlNegotiator.Tests.Stubs;
+using HandlebarsDotNet;
+using Microsoft.AspNetCore.Http;
+using Xunit;
+
+namespace Carter.HtmlNegotiator.Tests
+{
+ public class HandlebarsViewEngineTests
+ {
+ [Fact]
+ public void Should_Return_Compiled_HTML()
+ {
+ var handlebars = Handlebars.Create();
+ var viewResolver = new StubViewResolver(new Dictionary
+ {
+ ["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 result = subject.GetView(httpContext, new { Name = "Carter" });
+
+ 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 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("