diff --git a/.gitignore b/.gitignore
index aeebc7d..94b41b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,3 @@
-# Created by .ignore support plugin (hsz.mobi)
-### VisualStudio template
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
@@ -26,11 +24,14 @@ bld/
[Oo]bj/
[Ll]og/
-# Visual Studio 2015 cache/options directory
+# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
@@ -51,16 +52,21 @@ BenchmarkDotNet.Artifacts/
project.lock.json
project.fragment.lock.json
artifacts/
-**/Properties/launchSettings.json
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
*_i.c
*_p.c
-*_i.h
+*_h.h
*.ilk
*.meta
*.obj
+*.iobj
*.pch
*.pdb
+*.ipdb
*.pgc
*.pgd
*.rsp
@@ -214,6 +220,10 @@ ClientBin/
*.publishsettings
orleans.codegen.cs
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
@@ -228,6 +238,8 @@ _UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
# SQL Server files
*.mdf
@@ -238,6 +250,7 @@ UpgradeLog*.htm
*.rdl.data
*.bim.layout
*.bim_*.settings
+*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
@@ -249,9 +262,6 @@ FakesAssemblies/
.ntvs_analysis.dat
node_modules/
-# Typescript v1 declaration files
-typings/
-
# Visual Studio 6 build log
*.plg
@@ -306,4 +316,17 @@ __pycache__/
# OpenCover UI analysis results
OpenCover/
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+# Local History for Visual Studio
+.localhistory/
diff --git a/Carter.HtmlNegotiator.sln b/Carter.HtmlNegotiator.sln
new file mode 100644
index 0000000..97287cc
--- /dev/null
+++ b/Carter.HtmlNegotiator.sln
@@ -0,0 +1,87 @@
+
+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.csproj", "{0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dependencies", "dependencies", "{7B61769A-8EB6-4752-AF80-7F15558B2C76}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Carter", "Carter", "{C6E6F2C4-A88C-4E9D-9183-D3EB572423DE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carter", "dependencies\Carter\src\Carter.csproj", "{E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carter.HtmlNegotiator.Sample", "sample\Carter.HtmlNegotiator.Sample.csproj", "{C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{520CAA77-64DE-43BF-B173-6E1A8F3A40B9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlNegotiator.Tests", "tests\HtmlNegotiator.Tests\HtmlNegotiator.Tests.csproj", "{A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}"
+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
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Debug|x64.Build.0 = Debug|Any CPU
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Debug|x86.Build.0 = Debug|Any CPU
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Release|x64.ActiveCfg = Release|Any CPU
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Release|x64.Build.0 = Release|Any CPU
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Release|x86.ActiveCfg = Release|Any CPU
+ {0DBC4DD8-CC92-4C36-A6CA-ABEE717458CA}.Release|x86.Build.0 = Release|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Debug|x64.Build.0 = Debug|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Debug|x86.Build.0 = Debug|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Release|x64.ActiveCfg = Release|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Release|x64.Build.0 = Release|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Release|x86.ActiveCfg = Release|Any CPU
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3}.Release|x86.Build.0 = Release|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Debug|x64.Build.0 = Debug|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Debug|x86.Build.0 = Debug|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Release|x64.ActiveCfg = Release|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Release|x64.Build.0 = Release|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Release|x86.ActiveCfg = Release|Any CPU
+ {C0D65C1D-698E-4601-98D2-D0BA4E5C0F47}.Release|x86.Build.0 = Release|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Debug|x64.Build.0 = Debug|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Debug|x86.Build.0 = Debug|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Release|x64.ActiveCfg = Release|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Release|x64.Build.0 = Release|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Release|x86.ActiveCfg = Release|Any CPU
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {C6E6F2C4-A88C-4E9D-9183-D3EB572423DE} = {7B61769A-8EB6-4752-AF80-7F15558B2C76}
+ {E5B3E5AF-CA11-42EA-A9D0-72147B457AA3} = {C6E6F2C4-A88C-4E9D-9183-D3EB572423DE}
+ {A398FEB7-7F6E-4C98-9DA8-A35BE8D76C53} = {520CAA77-64DE-43BF-B173-6E1A8F3A40B9}
+ EndGlobalSection
+EndGlobal
diff --git a/Carter.HtmlNegotiator.sln.DotSettings b/Carter.HtmlNegotiator.sln.DotSettings
new file mode 100644
index 0000000..4df2510
--- /dev/null
+++ b/Carter.HtmlNegotiator.sln.DotSettings
@@ -0,0 +1,64 @@
+
+ True
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ WARNING
+ <?xml version="1.0" encoding="utf-16"?><Profile name="NancyStandard"><CSUseVar><BehavourStyle>CAN_CHANGE_TO_IMPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_IMPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_IMPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSReformatCode>True</CSReformatCode><CSShortenReferences>True</CSShortenReferences><CSReorderTypeMembers>True</CSReorderTypeMembers><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /></Profile>
+ Required
+ Required
+ Required
+ Required
+ Required
+ Required
+ Required
+ Required
+ All
+ False
+ NEXT_LINE
+ 1
+ 1
+ 1
+ 1
+ 1
+ 0
+ ALWAYS_ADD
+ ALWAYS_ADD
+ ALWAYS_ADD
+ ALWAYS_ADD
+ ALWAYS_ADD
+ ALWAYS_ADD
+ NEXT_LINE
+ 1
+ 1
+ True
+ NEVER
+ NEVER
+ ALWAYS_USE
+ LINE_BREAK
+ False
+ True
+ False
+ False
+ True
+ False
+ True
+ True
+
+
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="Aa_bb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ <data><IncludeFilters /><ExcludeFilters /></data>
+ <data />
+
diff --git a/dependencies/Carter b/dependencies/Carter
index 71ac6c5..aae223f 160000
--- a/dependencies/Carter
+++ b/dependencies/Carter
@@ -1 +1 @@
-Subproject commit 71ac6c595dba14057cb6a2dc8a94bd0b671a6ed7
+Subproject commit aae223f7507266d5bc598dc518f867c94d44bb8b
diff --git a/sample/Carter.HtmlNegotiator.Sample.csproj b/sample/Carter.HtmlNegotiator.Sample.csproj
new file mode 100644
index 0000000..e4bd6df
--- /dev/null
+++ b/sample/Carter.HtmlNegotiator.Sample.csproj
@@ -0,0 +1,20 @@
+
+
+ netcoreapp2.1
+ Carter.HtmlNegotiator.Sample
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/Features/MyFeature/MyFeatureModule.cs b/sample/Features/MyFeature/MyFeatureModule.cs
new file mode 100644
index 0000000..935ac55
--- /dev/null
+++ b/sample/Features/MyFeature/MyFeatureModule.cs
@@ -0,0 +1,16 @@
+namespace Carter.HtmlNegotiator.Sample.Features.MyFeature
+{
+ using ViewModels;
+
+ public class MyFeatureModule : CarterModule
+ {
+ public MyFeatureModule() : base("/myfeature")
+ {
+ this.Get("/",
+ async (req, res, routeData) =>
+ {
+ await res.AsHtml("index", new MyViewModel{Title = "Hello World!", Message = "Hello from a Custom View Location"});
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample/Features/MyFeature/views/index.hbs b/sample/Features/MyFeature/views/index.hbs
new file mode 100644
index 0000000..02a1977
--- /dev/null
+++ b/sample/Features/MyFeature/views/index.hbs
@@ -0,0 +1,9 @@
+
+
+
+ {{title}}
+
+
+ {{message}}
+
+
\ No newline at end of file
diff --git a/sample/Modules/AnotherModule.cs b/sample/Modules/AnotherModule.cs
new file mode 100644
index 0000000..6477625
--- /dev/null
+++ b/sample/Modules/AnotherModule.cs
@@ -0,0 +1,16 @@
+namespace Carter.HtmlNegotiator.Sample.Modules
+{
+ using ViewModels;
+
+ public class AnotherModule : CarterModule
+ {
+ public AnotherModule() : base("/another")
+ {
+ this.Get("/",
+ async (req, res, routeData) =>
+ {
+ await res.AsHtml("index", new MyViewModel{Title = "Hello World!", Message = "Hello from Another Module!"});
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample/Modules/HomeModule.cs b/sample/Modules/HomeModule.cs
new file mode 100644
index 0000000..3052b54
--- /dev/null
+++ b/sample/Modules/HomeModule.cs
@@ -0,0 +1,23 @@
+namespace Carter.HtmlNegotiator.Sample.Modules
+{
+ using Carter;
+ using ViewModels;
+
+ public class HomeModule : CarterModule
+ {
+ public HomeModule()
+ {
+ this.Get("/",
+ async (req, res, routeData) =>
+ {
+ await res.Negotiate("index",new MyViewModel{Title = "Hello World!", Message = "Hello from Carter.HtmlNegotiator!"});
+ });
+
+ this.Get("/custom",
+ async (req, res, routeData) =>
+ {
+ await res.AsHtml("myView", new MyViewModel{Title = "Custom View!", Message = "Hello from a custom view name!"});
+ });
+ }
+ }
+}
diff --git a/sample/Program.cs b/sample/Program.cs
new file mode 100644
index 0000000..49d2e3a
--- /dev/null
+++ b/sample/Program.cs
@@ -0,0 +1,19 @@
+namespace Carter.HtmlNegotiator.Sample
+{
+ using System.IO;
+ using Microsoft.AspNetCore.Hosting;
+
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseKestrel()
+ .UseStartup()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/sample/Startup.cs b/sample/Startup.cs
new file mode 100644
index 0000000..fb47113
--- /dev/null
+++ b/sample/Startup.cs
@@ -0,0 +1,25 @@
+namespace Carter.HtmlNegotiator.Sample
+{
+ using Carter;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Logging;
+
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddHtmlNegotiator(with =>
+ with.ViewLocation(ctx => $"features/{ctx.ModuleName}/views/{ctx.ViewName}"));
+ services.AddCarter();
+ }
+
+ public void Configure(IApplicationBuilder app, ILoggerFactory logging)
+ {
+ logging.AddConsole();
+ logging.AddDebug();
+
+ app.UseCarter();
+ }
+ }
+}
diff --git a/sample/ViewModels/MyViewModel.cs b/sample/ViewModels/MyViewModel.cs
new file mode 100644
index 0000000..db38af1
--- /dev/null
+++ b/sample/ViewModels/MyViewModel.cs
@@ -0,0 +1,8 @@
+namespace Carter.HtmlNegotiator.Sample.ViewModels
+{
+ public class MyViewModel
+ {
+ public string Title { get; set; }
+ public string Message { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/sample/Views/.DS_Store b/sample/Views/.DS_Store
new file mode 100644
index 0000000..3a8742d
Binary files /dev/null and b/sample/Views/.DS_Store differ
diff --git a/sample/Views/Another/index.hbs b/sample/Views/Another/index.hbs
new file mode 100644
index 0000000..02a1977
--- /dev/null
+++ b/sample/Views/Another/index.hbs
@@ -0,0 +1,9 @@
+
+
+
+ {{title}}
+
+
+ {{message}}
+
+
\ No newline at end of file
diff --git a/sample/Views/Home/index.hbs b/sample/Views/Home/index.hbs
new file mode 100644
index 0000000..02a1977
--- /dev/null
+++ b/sample/Views/Home/index.hbs
@@ -0,0 +1,9 @@
+
+
+
+ {{title}}
+
+
+ {{message}}
+
+
\ No newline at end of file
diff --git a/sample/Views/Home/myView.hbs b/sample/Views/Home/myView.hbs
new file mode 100644
index 0000000..02a1977
--- /dev/null
+++ b/sample/Views/Home/myView.hbs
@@ -0,0 +1,9 @@
+
+
+
+ {{title}}
+
+
+ {{message}}
+
+
\ No newline at end of file
diff --git a/src/AmbiguousViewsException.cs b/src/AmbiguousViewsException.cs
new file mode 100644
index 0000000..d10f7a9
--- /dev/null
+++ b/src/AmbiguousViewsException.cs
@@ -0,0 +1,9 @@
+namespace Carter.HtmlNegotiator
+{
+ using System;
+
+ public class AmbiguousViewsException : Exception
+ {
+ public AmbiguousViewsException(string message) : base(message) {}
+ }
+}
\ No newline at end of file
diff --git a/src/Carter.HtmlNegotiator.csproj b/src/Carter.HtmlNegotiator.csproj
index 9f5c4f4..c46b5bb 100644
--- a/src/Carter.HtmlNegotiator.csproj
+++ b/src/Carter.HtmlNegotiator.csproj
@@ -1,7 +1,17 @@
-
+
netstandard2.0
+ 7.1
+
+
+
+
+
+
+
+
+
diff --git a/src/DefaultViewLocator.cs b/src/DefaultViewLocator.cs
index a89e056..3568869 100644
--- a/src/DefaultViewLocator.cs
+++ b/src/DefaultViewLocator.cs
@@ -1,54 +1,76 @@
-namespace HtmlNegotiator
+namespace Carter.HtmlNegotiator
{
using System;
using System.Collections.Generic;
using System.IO;
- using Microsoft.AspNetCore.Hosting;
- using Microsoft.AspNetCore.Http;
+ using System.Linq;
public class DefaultViewLocator : IViewLocator
{
- private readonly IDictionary mappings;
+ private readonly IEnumerable viewEngines;
+ private readonly IDirectoryService directoryService;
+
+ private static readonly IDictionary ViewCache = new Dictionary();
- private readonly IDictionary htmlMappings;
+ public DefaultViewLocator(IEnumerable viewEngines, IDirectoryService directoryService)
+ {
+ this.viewEngines = viewEngines;
+ this.directoryService = directoryService;
+ }
+
+ public ViewTemplate LocateView(string viewLocation)
+ {
+ return GetViewFromCache(viewLocation)
+ ?? this.LocateAndCacheView(viewLocation);
+ }
- public DefaultViewLocator(IDictionary mappings)
+ private static ViewTemplate GetViewFromCache(string viewLocation)
{
- this.mappings = mappings;
- this.htmlMappings = new Dictionary();
+ return ViewCache.ContainsKey(viewLocation)
+ ? ViewCache[viewLocation]
+ : null;
}
- public string GetView(object model, HttpContext httpContext)
+ private ViewTemplate LocateAndCacheView(string viewLocation)
{
- string viewName = string.Empty;
+ var supportedExtensions = this.viewEngines.SelectMany(e => e.SupportedExtensions).Distinct().ToList();
+
+ var viewName = Path.GetFileNameWithoutExtension(viewLocation);
+ var path = viewLocation.Substring(0, viewLocation.LastIndexOf(viewName, StringComparison.Ordinal));
+
+ IList viewTemplates = null;
try
{
- viewName = this.mappings[model.GetType()];
+ viewTemplates = this.directoryService.GetViews(path, viewName, supportedExtensions).ToList();
}
- catch (Exception)
+ catch (DirectoryNotFoundException)
{
- return string.Empty;
}
-
- if (this.htmlMappings.ContainsKey(model.GetType()))
+
+ if (viewTemplates?.Count == 1)
{
- return this.htmlMappings[model.GetType()];
+ var viewTemplate = viewTemplates.Single();
+ ViewCache.Add(viewLocation, viewTemplate);
+ return viewTemplate;
}
- var env = (IHostingEnvironment)httpContext.RequestServices.GetService(typeof(IHostingEnvironment));
-
- try
+ if (viewTemplates?.Count > 1)
{
- var html = File.ReadAllText(Path.Combine(env.ContentRootPath, viewName));
+ throw new AmbiguousViewsException(GetAmbiguousViewsExceptionMessage(viewTemplates));
+ }
- this.htmlMappings.Add(model.GetType(), html);
+ return null;
+ }
- return html;
- }
- catch (FileNotFoundException)
- {
- return string.Empty;
- }
+ private static string GetAmbiguousViewsExceptionMessage(IEnumerable viewTemplates)
+ {
+ return
+ $"Multiple views found.{Environment.NewLine}Views:{Environment.NewLine}{string.Join(Environment.NewLine, viewTemplates.Select(GethFullPath))}";
+ }
+
+ private static string GethFullPath(ViewTemplate template)
+ {
+ return string.Concat(template.Location, "/", template.Name, ".", template.Extension);
}
}
}
diff --git a/src/DirectoryService.cs b/src/DirectoryService.cs
new file mode 100644
index 0000000..2eab47a
--- /dev/null
+++ b/src/DirectoryService.cs
@@ -0,0 +1,30 @@
+namespace Carter.HtmlNegotiator
+{
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+
+ public class DirectoryService : IDirectoryService
+ {
+ public IEnumerable GetViews(string path, string viewname, IEnumerable extensions)
+ {
+ var files = Directory.GetFiles(path, $"{viewname}.*", SearchOption.TopDirectoryOnly);
+ return files
+ .Where(filename => this.IsValidExtension(filename, extensions))
+ .Select(file => new ViewTemplate
+ {
+ Name = viewname,
+ Location = path,
+ Extension = Path.GetExtension(file),
+ Source = () => File.ReadAllText(file)
+ }).ToList();
+ }
+
+ private bool IsValidExtension(string filename, IEnumerable supportedExtensions)
+ {
+ var extension = Path.GetExtension(filename);
+ return !string.IsNullOrEmpty(extension)
+ && supportedExtensions.Contains(extension.TrimStart('.'));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HandlebarsViewEngine.cs b/src/HandlebarsViewEngine.cs
new file mode 100644
index 0000000..206d0fb
--- /dev/null
+++ b/src/HandlebarsViewEngine.cs
@@ -0,0 +1,21 @@
+namespace Carter.HtmlNegotiator
+{
+ using System.Collections.Generic;
+ using HandlebarsDotNet;
+
+ public class HandlebarsViewEngine : IViewEngine
+ {
+ public IEnumerable SupportedExtensions { get; }
+
+ public HandlebarsViewEngine()
+ {
+ this.SupportedExtensions = new List{ "hbs" };
+ }
+
+ public string Render(ViewTemplate viewTemplate, object model)
+ {
+ var template = Handlebars.Compile(viewTemplate.Source());
+ return template(model);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HtmlNegotiator.cs b/src/HtmlNegotiator.cs
index 89f46b1..c4df0d8 100644
--- a/src/HtmlNegotiator.cs
+++ b/src/HtmlNegotiator.cs
@@ -1,19 +1,18 @@
-namespace HtmlNegotiator
+namespace Carter.HtmlNegotiator
{
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Carter;
- using HandlebarsDotNet;
using Microsoft.AspNetCore.Http;
public class HtmlNegotiator : IResponseNegotiator
{
- private readonly IViewLocator viewLocator;
+ private readonly IViewRenderer viewRenderer;
- public HtmlNegotiator(IViewLocator viewLocator)
+ public HtmlNegotiator(IViewRenderer viewRenderer)
{
- this.viewLocator = viewLocator;
+ this.viewRenderer = viewRenderer;
}
public bool CanHandle(Microsoft.Net.Http.Headers.MediaTypeHeaderValue accept)
@@ -23,20 +22,19 @@ public bool CanHandle(Microsoft.Net.Http.Headers.MediaTypeHeaderValue accept)
public async Task Handle(HttpRequest req, HttpResponse res, object model, CancellationToken cancellationToken)
{
- var source = viewLocator.GetView(model, res.HttpContext);
- if (string.IsNullOrEmpty(source))
+ var view = this.viewRenderer.RenderView(req.HttpContext, model);
+
+ if (string.IsNullOrEmpty(view))
{
res.StatusCode = 500;
res.ContentType = "text/plain";
await res.WriteAsync("View not found", cancellationToken);
+ return;
}
- var template = Handlebars.Compile(source);
-
res.ContentType = "text/html";
res.StatusCode = (int)HttpStatusCode.OK;
-
- await res.WriteAsync(template(model), cancellationToken);
+ await res.WriteAsync(view, cancellationToken);
}
}
}
diff --git a/src/HtmlNegotiatorConfiguration.cs b/src/HtmlNegotiatorConfiguration.cs
new file mode 100644
index 0000000..8f5e52d
--- /dev/null
+++ b/src/HtmlNegotiatorConfiguration.cs
@@ -0,0 +1,27 @@
+namespace Carter.HtmlNegotiator
+{
+ using System;
+ using System.Collections.Generic;
+
+ public class HtmlNegotiatorConfiguration
+ {
+ public HtmlNegotiatorConfiguration(Action configuration = default)
+ {
+ this.ViewLocationConventions = new List>
+ {
+ viewLocationContext => viewLocationContext.ViewName,
+ viewLocationContext => $"views/{viewLocationContext.ViewName}",
+ viewLocationContext => $"{viewLocationContext.ModuleName}/{viewLocationContext.ViewName}",
+ viewLocationContext => $"views/{viewLocationContext.ModuleName}/{viewLocationContext.ViewName}"
+ };
+
+ if (configuration != null)
+ {
+ var configurator = new HtmlNegotiatorConfigurator(this);
+ configuration.Invoke(configurator);
+ }
+ }
+
+ public IList> ViewLocationConventions { get; set; }
+ }
+}
diff --git a/src/HtmlNegotiatorConfigurator.cs b/src/HtmlNegotiatorConfigurator.cs
new file mode 100644
index 0000000..319dbe4
--- /dev/null
+++ b/src/HtmlNegotiatorConfigurator.cs
@@ -0,0 +1,20 @@
+namespace Carter.HtmlNegotiator
+{
+ using System;
+
+ public class HtmlNegotiatorConfigurator
+ {
+ private readonly HtmlNegotiatorConfiguration configuration;
+
+ public HtmlNegotiatorConfigurator(HtmlNegotiatorConfiguration configuration)
+ {
+ this.configuration = configuration;
+ }
+
+ public HtmlNegotiatorConfigurator ViewLocation(Func convention)
+ {
+ this.configuration.ViewLocationConventions.Add(convention);
+ return this;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/IDirectoryService.cs b/src/IDirectoryService.cs
new file mode 100644
index 0000000..7cc2986
--- /dev/null
+++ b/src/IDirectoryService.cs
@@ -0,0 +1,9 @@
+namespace Carter.HtmlNegotiator
+{
+ using System.Collections.Generic;
+
+ public interface IDirectoryService
+ {
+ IEnumerable GetViews(string path, string viewname, IEnumerable extensions);
+ }
+}
\ No newline at end of file
diff --git a/src/IServiceCollectionExtentions.cs b/src/IServiceCollectionExtentions.cs
new file mode 100644
index 0000000..162b2d6
--- /dev/null
+++ b/src/IServiceCollectionExtentions.cs
@@ -0,0 +1,25 @@
+namespace Carter.HtmlNegotiator
+{
+ using System;
+ using Carter;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class ServiceCollectionExtentions
+ {
+ public static void AddHtmlNegotiator(this IServiceCollection services, Action configuration = default)
+ {
+ AddDefaultServices(services, new HtmlNegotiatorConfiguration(configuration));
+ }
+
+ private static void AddDefaultServices(IServiceCollection services, HtmlNegotiatorConfiguration config)
+ {
+ services.AddSingleton(config);
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/IViewEngine.cs b/src/IViewEngine.cs
new file mode 100644
index 0000000..063d34b
--- /dev/null
+++ b/src/IViewEngine.cs
@@ -0,0 +1,10 @@
+namespace Carter.HtmlNegotiator
+{
+ using System.Collections.Generic;
+
+ public interface IViewEngine
+ {
+ IEnumerable SupportedExtensions { get; }
+ string Render(ViewTemplate viewTemplate, object model);
+ }
+}
\ No newline at end of file
diff --git a/src/IViewLocator.cs b/src/IViewLocator.cs
index c72c9c9..b85fc43 100644
--- a/src/IViewLocator.cs
+++ b/src/IViewLocator.cs
@@ -1,9 +1,10 @@
-namespace HtmlNegotiator
+namespace Carter.HtmlNegotiator
{
using Microsoft.AspNetCore.Http;
public interface IViewLocator
{
- string GetView(object model, HttpContext httpContext);
+ ViewTemplate LocateView(string viewLocation);
+ //string LocateView(object model, HttpContext httpContext);
}
}
diff --git a/src/IViewRenderer.cs b/src/IViewRenderer.cs
new file mode 100644
index 0000000..e9a2adc
--- /dev/null
+++ b/src/IViewRenderer.cs
@@ -0,0 +1,9 @@
+namespace Carter.HtmlNegotiator
+{
+ using Microsoft.AspNetCore.Http;
+
+ public interface IViewRenderer
+ {
+ string RenderView(HttpContext httpContext, object model);
+ }
+}
\ No newline at end of file
diff --git a/src/IViewResolver.cs b/src/IViewResolver.cs
new file mode 100644
index 0000000..448953f
--- /dev/null
+++ b/src/IViewResolver.cs
@@ -0,0 +1,7 @@
+namespace Carter.HtmlNegotiator
+{
+ public interface IViewResolver
+ {
+ ViewTemplate ResolveView(ViewLocationContext viewLocationContext);
+ }
+}
\ No newline at end of file
diff --git a/src/ResponseExtensions.cs b/src/ResponseExtensions.cs
new file mode 100644
index 0000000..63651cc
--- /dev/null
+++ b/src/ResponseExtensions.cs
@@ -0,0 +1,41 @@
+namespace Carter.HtmlNegotiator
+{
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Carter;
+ using Carter.Response;
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Net.Http.Headers;
+
+ public static class ResponseExtensions
+ {
+ public static async Task Negotiate(this HttpResponse response, string view, object obj, CancellationToken cancellationToken = default)
+ {
+ MediaTypeHeaderValue.TryParseList(response.HttpContext.Request.Headers["Accept"], out var accept);
+ if (accept != null)
+ {
+ var ordered = accept.OrderByDescending(x => x.Quality ?? 1);
+
+ var first = ordered.First();
+
+ if (first.SubType == "html")
+ {
+ await response.AsHtml(view, obj, cancellationToken);
+ return;
+ }
+ }
+
+ await response.Negotiate(obj, cancellationToken);
+ }
+
+ public static async Task AsHtml(this HttpResponse response, string view, object obj, CancellationToken cancellationToken = default)
+ {
+ response.HttpContext.Items.Add("View", view);
+ var negotiators = response.HttpContext.RequestServices.GetServices();
+ var negotiator = negotiators.FirstOrDefault(x => x.CanHandle(new MediaTypeHeaderValue("text/html")));
+ await negotiator.Handle(response.HttpContext.Request, response, obj, cancellationToken);
+ }
+ }
+}
diff --git a/src/ViewLocationContext.cs b/src/ViewLocationContext.cs
new file mode 100644
index 0000000..ec5003b
--- /dev/null
+++ b/src/ViewLocationContext.cs
@@ -0,0 +1,9 @@
+namespace Carter.HtmlNegotiator
+{
+ public class ViewLocationContext
+ {
+ public string ViewName { get; set; }
+ public string ModuleName { get; set; }
+ public string RootPath { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ViewRenderer.cs b/src/ViewRenderer.cs
new file mode 100644
index 0000000..b03ff55
--- /dev/null
+++ b/src/ViewRenderer.cs
@@ -0,0 +1,63 @@
+namespace Carter.HtmlNegotiator
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text.RegularExpressions;
+ using Microsoft.AspNetCore.Hosting;
+ using Microsoft.AspNetCore.Http;
+
+ public class ViewRenderer : IViewRenderer
+ {
+ private readonly IViewResolver viewResolver;
+ private readonly IEnumerable viewEngines;
+
+ public ViewRenderer(IViewResolver viewResolver, IEnumerable viewEngines)
+ {
+ this.viewResolver = viewResolver;
+ this.viewEngines = viewEngines;
+ }
+
+ public string RenderView(HttpContext httpContext, object model)
+ {
+ var viewTemplate = this.viewResolver.ResolveView(this.GetViewLocationContext(httpContext, model));
+ if (viewTemplate == null)
+ {
+ return null;
+ }
+ var viewEngine = this.viewEngines.FirstOrDefault(ve => ve.SupportedExtensions.Any(e => e.Equals(viewTemplate.Extension.TrimStart('.'), StringComparison.OrdinalIgnoreCase)));
+ return viewEngine.Render(viewTemplate, model);
+ }
+
+ private ViewLocationContext GetViewLocationContext(HttpContext httpContext, object model)
+ {
+ return new ViewLocationContext
+ {
+ RootPath = ((IHostingEnvironment)httpContext.RequestServices.GetService(typeof(IHostingEnvironment))).ContentRootPath,
+ ViewName = this.GetViewName(httpContext, model),
+ ModuleName = this.GetModuleName(httpContext.Items["ModuleType"] as Type)
+ };
+ }
+
+ private string GetModuleName(Type moduleType)
+ {
+ var moduleTypeName = moduleType.ToString().Split('.').Last().ToLower();
+ return moduleTypeName.Substring(0, moduleTypeName.IndexOf("module", StringComparison.Ordinal));
+ }
+
+ private string GetViewName(HttpContext httpContext, object model)
+ {
+ if (!httpContext.Items.ContainsKey("View") && model == null)
+ {
+ return "index";
+ }
+
+ if (!httpContext.Items.ContainsKey("View"))
+ {
+ return Regex.Replace(model.GetType().Name, "ViewModel|Model", string.Empty);
+ }
+
+ return httpContext.Items["View"].ToString();
+ }
+ }
+}
diff --git a/src/ViewResolver.cs b/src/ViewResolver.cs
new file mode 100644
index 0000000..b3b334a
--- /dev/null
+++ b/src/ViewResolver.cs
@@ -0,0 +1,38 @@
+namespace Carter.HtmlNegotiator
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+
+ public class ViewResolver : IViewResolver
+ {
+ private readonly IViewLocator viewLocator;
+ private readonly IEnumerable> conventions;
+
+ public ViewResolver(IViewLocator viewLocator, HtmlNegotiatorConfiguration configuration)
+ {
+ this.viewLocator = viewLocator;
+ this.conventions = configuration.ViewLocationConventions;
+ }
+ public ViewTemplate ResolveView(ViewLocationContext viewLocationContext)
+ {
+ foreach (var convention in this.conventions)
+ {
+ var viewLocation = convention.Invoke(viewLocationContext);
+
+ if (string.IsNullOrEmpty(viewLocation))
+ {
+ continue;
+ }
+
+ var viewTemplate = this.viewLocator.LocateView(Path.Combine(viewLocationContext.RootPath, viewLocation));
+ if (viewTemplate != null)
+ {
+ return viewTemplate;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/ViewTemplate.cs b/src/ViewTemplate.cs
new file mode 100644
index 0000000..4242250
--- /dev/null
+++ b/src/ViewTemplate.cs
@@ -0,0 +1,15 @@
+namespace Carter.HtmlNegotiator
+{
+ using System;
+
+ public class ViewTemplate
+ {
+ public string Name { get; set; }
+
+ public Func Source { get; set; }
+
+ public string Location { get; set; }
+
+ public string Extension { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/tests/HtmlNegotiator.Tests/HtmlNegotiator.Tests.csproj b/tests/HtmlNegotiator.Tests/HtmlNegotiator.Tests.csproj
new file mode 100644
index 0000000..ca40b42
--- /dev/null
+++ b/tests/HtmlNegotiator.Tests/HtmlNegotiator.Tests.csproj
@@ -0,0 +1,21 @@
+
+
+
+ netcoreapp2.1
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/HtmlNegotiator.Tests/HtmlNegotiatorTests.cs b/tests/HtmlNegotiator.Tests/HtmlNegotiatorTests.cs
new file mode 100644
index 0000000..829095b
--- /dev/null
+++ b/tests/HtmlNegotiator.Tests/HtmlNegotiatorTests.cs
@@ -0,0 +1,91 @@
+namespace HtmlNegotiator.Tests
+{
+ using System.IO;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Carter.HtmlNegotiator;
+ using FakeItEasy;
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.Net.Http.Headers;
+ using Xunit;
+
+ public class HtmlNegotiatorTests
+ {
+ private readonly IViewRenderer viewRenderer;
+ private readonly HtmlNegotiator htmlNegotiator;
+
+ public HtmlNegotiatorTests()
+ {
+ this.viewRenderer = A.Fake();
+ this.htmlNegotiator = new HtmlNegotiator(this.viewRenderer);
+ }
+
+ [Fact]
+ public void Should_Be_Able_To_Handle_Requests_With_A_HTML_MediaType()
+ {
+ // ARRANGE
+ var headerValue = new MediaTypeHeaderValue("text/html");
+
+ // ACT
+ var result = this.htmlNegotiator.CanHandle(headerValue);
+
+ // ASSERT
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Should_Not_Be_Able_To_Handle_Requests_Other_MediaTypes()
+ {
+ // ARRANGE
+ var headerValue = new MediaTypeHeaderValue("application/not-html");
+
+ // ACT
+ var result = this.htmlNegotiator.CanHandle(headerValue);
+
+ // ASSERT
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task Should_Write_A_HTML_Response_When_A_View_Has_Been_Rendered()
+ {
+ // ARRANGE
+ var httpContext = A.Fake();
+ var httpResponse = A.Fake();
+ var httpRequest = A.Fake();
+
+ A.CallTo(() => httpResponse.Body).Returns(new MemoryStream());
+ A.CallTo(() => httpRequest.HttpContext).Returns(httpContext);
+ A.CallTo(() => this.viewRenderer.RenderView(A.Ignored, A