General razor clean up

* Change Razor compilation to use ApplicationBasePath to determine the
  app root
* Change class name generation to be based on app-relative path.
This commit is contained in:
Pranav K 2014-03-07 11:21:46 -08:00
parent 2731caf476
commit 123089d5c7
16 changed files with 176 additions and 79 deletions

View File

@ -55,6 +55,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.net45", "src\Common\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.k10", "src\Common\Common.k10.csproj", "{D4576205-A5B5-4382-BB34-19DE9855FE94}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Mvc.Razor.Test.net45", "test\Microsoft.AspNet.Mvc.Razor.Test\Microsoft.AspNet.Mvc.Razor.Test.net45.csproj", "{3EB2CFF9-6E67-4C03-9AC4-2DD169024938}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestCommon.net45", "test\TestCommon\TestCommon.net45.csproj", "{75A07B53-C5EE-4995-A55B-27562C23BCCD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -137,6 +141,14 @@ Global
{D4576205-A5B5-4382-BB34-19DE9855FE94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4576205-A5B5-4382-BB34-19DE9855FE94}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4576205-A5B5-4382-BB34-19DE9855FE94}.Release|Any CPU.Build.0 = Release|Any CPU
{3EB2CFF9-6E67-4C03-9AC4-2DD169024938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3EB2CFF9-6E67-4C03-9AC4-2DD169024938}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3EB2CFF9-6E67-4C03-9AC4-2DD169024938}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3EB2CFF9-6E67-4C03-9AC4-2DD169024938}.Release|Any CPU.Build.0 = Release|Any CPU
{75A07B53-C5EE-4995-A55B-27562C23BCCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75A07B53-C5EE-4995-A55B-27562C23BCCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75A07B53-C5EE-4995-A55B-27562C23BCCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75A07B53-C5EE-4995-A55B-27562C23BCCD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -165,5 +177,7 @@ Global
{A7D7CD66-A407-4144-8AB7-07F895F87137} = {CE037E26-9EB5-48E2-B73B-06C6FF6CC9F5}
{42195A56-42C0-4CFF-A982-B6E24EFC6356} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{537CC0EE-4B62-4789-9AE9-94BE28E0D25A} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{3EB2CFF9-6E67-4C03-9AC4-2DD169024938} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{75A07B53-C5EE-4995-A55B-27562C23BCCD} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
EndGlobalSection
EndGlobal

View File

@ -5,6 +5,6 @@ namespace Microsoft.AspNet.Mvc.Razor
{
public interface IMvcRazorHost
{
GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream);
GeneratorResults GenerateCode(string rootNamespace, string rootRelativePath, Stream inputStream);
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Text;
using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator;
using Microsoft.AspNet.Razor.Parser;
@ -50,19 +49,13 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
public GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream)
public GeneratorResults GenerateCode(string rootNamespace, string rootRelativePath, Stream inputStream)
{
string className = Path.GetFileNameWithoutExtension(rootRelativePath);
if (rootRelativePath.StartsWith("~/", StringComparison.Ordinal))
{
rootRelativePath = rootRelativePath.Substring(2);
}
string classNamespace = GenerateNamespace(rootRelativePath);
string className = ParserHelpers.SanitizeClassName(rootRelativePath);
using (var reader = new StreamReader(inputStream))
{
var engine = new RazorTemplateEngine(this);
return engine.GenerateCode(reader, className, classNamespace, rootRelativePath);
return engine.GenerateCode(reader, className, rootNamespace, rootRelativePath);
}
}
@ -70,28 +63,5 @@ namespace Microsoft.AspNet.Mvc.Razor
{
return new MvcRazorCodeParser(_baseType);
}
private static string GenerateNamespace(string rootRelativePath)
{
var namespaceBuilder = new StringBuilder(rootRelativePath.Length);
rootRelativePath = Path.GetDirectoryName(rootRelativePath);
for (int i = 0; i < rootRelativePath.Length; i++)
{
char c = rootRelativePath[i];
if (c == Path.DirectorySeparatorChar)
{
namespaceBuilder.Append('.');
}
else if (!Char.IsLetterOrDigit(c))
{
namespaceBuilder.Append('_');
}
else
{
namespaceBuilder.Append(c);
}
}
return namespaceBuilder.ToString();
}
}
}

View File

@ -26,6 +26,38 @@ namespace Microsoft.AspNet.Mvc.Razor
return GetString("ArgumentCannotBeNullOrEmpty");
}
/// <summary>
/// The layout view '{0}' could not be located.
/// </summary>
internal static string LayoutCannotBeLocated
{
get { return GetString("LayoutCannotBeLocated"); }
}
/// <summary>
/// The layout view '{0}' could not be located.
/// </summary>
internal static string FormatLayoutCannotBeLocated(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("LayoutCannotBeLocated"), p0);
}
/// <summary>
/// RenderBody can only be called from a layout page.
/// </summary>
internal static string RenderBodyCannotBeCalled
{
get { return GetString("RenderBodyCannotBeCalled"); }
}
/// <summary>
/// RenderBody can only be called from a layout page.
/// </summary>
internal static string FormatRenderBodyCannotBeCalled()
{
return GetString("RenderBodyCannotBeCalled");
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -5,6 +5,6 @@ namespace Microsoft.AspNet.Mvc.Razor
{
public interface IRazorCompilationService
{
Task<CompilationResult> Compile(string appRoot, IFileInfo fileInfo);
Task<CompilationResult> Compile(IFileInfo fileInfo);
}
}

View File

@ -5,48 +5,60 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Razor;
using Microsoft.Net.Runtime;
namespace Microsoft.AspNet.Mvc.Razor
{
public class RazorCompilationService : IRazorCompilationService
{
private static readonly CompilerCache _cache = new CompilerCache();
private readonly IApplicationEnvironment _environment;
private readonly ICompilationService _baseCompilationService;
private readonly IMvcRazorHost _razorHost;
private readonly string _appRoot;
public RazorCompilationService(ICompilationService compilationService, IMvcRazorHost razorHost)
public RazorCompilationService(IApplicationEnvironment environment,
ICompilationService compilationService,
IMvcRazorHost razorHost)
{
_environment = environment;
_baseCompilationService = compilationService;
_razorHost = razorHost;
_appRoot = EnsureTrailingSlash(environment.ApplicationBasePath);
}
public Task<CompilationResult> Compile(string appRoot, IFileInfo file)
public Task<CompilationResult> Compile([NotNull]IFileInfo file)
{
return _cache.GetOrAdd(file, () => CompileCore(appRoot, file));
return _cache.GetOrAdd(file, () => CompileCore(file));
}
private async Task<CompilationResult> CompileCore(string appRoot, IFileInfo file)
// TODO: Make this internal
public async Task<CompilationResult> CompileCore(IFileInfo file)
{
GeneratorResults results;
using (Stream inputStream = file.CreateReadStream())
{
Contract.Assert(file.PhysicalPath.StartsWith(appRoot, StringComparison.OrdinalIgnoreCase));
// Remove the app name segment so that it appears as part of the root relative path:
// work/src/myapp/ -> work/src
// root relative path: myapp/views/home/index.cshtml
// TODO: The root namespace might be a property we'd have to read via configuration since it
// affects other things such as resx files.
appRoot = Path.GetDirectoryName(appRoot.TrimEnd(Path.DirectorySeparatorChar));
string rootRelativePath = file.PhysicalPath.Substring(appRoot.Length).TrimStart(Path.DirectorySeparatorChar);
results = _razorHost.GenerateCode(rootRelativePath, inputStream);
Contract.Assert(file.PhysicalPath.StartsWith(_appRoot, StringComparison.OrdinalIgnoreCase));
var rootRelativePath = file.PhysicalPath.Substring(_appRoot.Length);
results = _razorHost.GenerateCode(_environment.ApplicationName, rootRelativePath, inputStream);
}
if (!results.Success)
{
return CompilationResult.Failed(results.GeneratedCode, results.ParserErrors.Select(e => new CompilationMessage(e.Message)));
var messages = results.ParserErrors.Select(e => new CompilationMessage(e.Message));
throw new CompilationFailedException(messages, results.GeneratedCode);
}
return await _baseCompilationService.Compile(results.GeneratedCode);
}
private static string EnsureTrailingSlash([NotNull]string path)
{
if (!path.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
path += Path.DirectorySeparatorChar;
}
return path;
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
@ -29,8 +28,8 @@ namespace Microsoft.AspNet.Mvc.Razor
Execute();
}
string bodyContent = contentBuilder.ToString();
if (!String.IsNullOrEmpty(Layout))
var bodyContent = contentBuilder.ToString();
if (!string.IsNullOrEmpty(Layout))
{
await RenderLayoutAsync(context, writer, bodyContent);
}
@ -43,10 +42,11 @@ namespace Microsoft.AspNet.Mvc.Razor
private async Task RenderLayoutAsync(ViewContext context, TextWriter writer, string bodyContent)
{
var virtualPathFactory = context.ServiceProvider.GetService<IVirtualPathViewFactory>();
RazorView layoutView = (RazorView)(await virtualPathFactory.CreateInstance(Layout));
var layoutView = (RazorView)(await virtualPathFactory.CreateInstance(Layout));
if (layoutView == null)
{
string message = String.Format(CultureInfo.CurrentCulture, "The layout view '{0}' could not be located.", Layout);
var message = Resources.FormatLayoutCannotBeLocated(Layout);
throw new InvalidOperationException(message);
}
@ -66,18 +66,9 @@ namespace Microsoft.AspNet.Mvc.Razor
if (content != null)
{
var htmlString = content as HtmlString;
if (htmlString != null)
{
writer.Write(content.ToString());
}
else
{
#if NET45
WebUtility.HtmlEncode(content.ToString(), writer);
#else
writer.Write(WebUtility.HtmlEncode(content.ToString()));
#endif
}
var contentToWrite = htmlString != null ? content.ToString() :
WebUtility.HtmlEncode(content.ToString());
writer.Write(contentToWrite);
}
}
@ -196,7 +187,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{
if (BodyContent == null)
{
throw new InvalidOperationException("RenderBody cannot be called at this point because you're not executing a layout");
throw new InvalidOperationException(Resources.RenderBodyCannotBeCalled);
}
return new HtmlString(BodyContent);
}

View File

@ -120,4 +120,10 @@
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
<value>Value cannot be null or empty.</value>
</data>
<data name="LayoutCannotBeLocated" xml:space="preserve">
<value>The layout view '{0}' could not be located.</value>
</data>
<data name="RenderBodyCannotBeCalled" xml:space="preserve">
<value>RenderBody can only be called from a layout page.</value>
</data>
</root>

View File

@ -10,22 +10,20 @@ namespace Microsoft.AspNet.Mvc.Razor
private readonly PhysicalFileSystem _fileSystem;
private readonly IRazorCompilationService _compilationService;
public VirtualPathViewFactory(IApplicationEnvironment env, IRazorCompilationService compilationService)
public VirtualPathViewFactory(IApplicationEnvironment env,
IRazorCompilationService compilationService)
{
// TODO: Continue to inject the IFileSystem but only when we get it from the host
_fileSystem = new PhysicalFileSystem(env.ApplicationBasePath);
_compilationService = compilationService;
}
public async Task<IView> CreateInstance(string virtualPath)
public async Task<IView> CreateInstance([NotNull]string virtualPath)
{
// TODO: We need to glean the approot from HttpContext
var appRoot = _fileSystem.Root;
IFileInfo fileInfo;
if (_fileSystem.TryGetFileInfo(virtualPath, out fileInfo))
{
CompilationResult result = await _compilationService.Compile(appRoot, fileInfo);
CompilationResult result = await _compilationService.Compile(fileInfo);
return (IView)Activator.CreateInstance(result.CompiledType);
}

View File

@ -34,34 +34,34 @@ namespace Microsoft.AspNet.Mvc.Razor
// TODO: We plan to change this on the next CR, so we don't have a strong depenedency directly on the specific
// type of the action descriptor
ActionDescriptor actionDescriptor = actionContext.ActionDescriptor;
var actionDescriptor = actionContext.ActionDescriptor;
if (actionDescriptor == null)
{
return null;
}
if (String.IsNullOrEmpty(viewName))
if (string.IsNullOrEmpty(viewName))
{
viewName = actionDescriptor.Name;
}
if (String.IsNullOrEmpty(viewName))
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "viewName");
}
bool nameRepresentsPath = IsSpecificPath(viewName);
var nameRepresentsPath = IsSpecificPath(viewName);
if (nameRepresentsPath)
{
IView view = await _virtualPathFactory.CreateInstance(viewName);
var view = await _virtualPathFactory.CreateInstance(viewName);
return view != null ? ViewEngineResult.Found(view) :
ViewEngineResult.NotFound(new[] { viewName });
}
else
{
string controllerName = actionDescriptor.Path;
var controllerName = actionDescriptor.Path;
var searchedLocations = new List<string>(_viewLocationFormats.Length);
for (int i = 0; i < _viewLocationFormats.Length; i++)
{

View File

@ -4,6 +4,7 @@
"Microsoft.AspNet.Abstractions": "0.1-alpha-*",
"Microsoft.AspNet.PipelineCore": "0.1-alpha-*",
"Microsoft.AspNet.Mvc.ModelBinding" : "",
"TestCommon" : "",
"Moq": "4.0.10827",
"Xunit": "1.9.1",
"Xunit.extensions": "1.9.1"

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.Net.Runtime;
using Moq;
using Xunit.Extensions;
namespace Microsoft.AspNet.Mvc.Razor.Test
{
public class RazorCompilationServiceTest
{
[Theory]
[InlineData(@"src\work\myapp", @"src\work\myapp\views\index\home.cshtml")]
[InlineData(@"src\work\myapp\", @"src\work\myapp\views\index\home.cshtml")]
public void CompileCoreCalculatesRootRelativePath(string appPath, string viewPath)
{
// Arrange
var env = new Mock<IApplicationEnvironment>();
env.SetupGet(e => e.ApplicationName).Returns("MyTestApplication");
env.SetupGet(e => e.ApplicationBasePath).Returns(appPath);
var host = new Mock<IMvcRazorHost>();
host.Setup(h => h.GenerateCode("MyTestApplication", @"views\index\home.cshtml", It.IsAny<Stream>()))
.Returns(new GeneratorResults(new Block(new BlockBuilder { Type = BlockType.Comment }), new RazorError[0], new CodeBuilderResult("", new LineMapping[0])))
.Verifiable();
var compiler = new Mock<ICompilationService>();
compiler.Setup(c => c.Compile(It.IsAny<string>()))
.Returns(Task.FromResult(CompilationResult.Successful("", typeof(RazorCompilationServiceTest))));
var razorService = new RazorCompilationService(env.Object, compiler.Object, host.Object);
var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.PhysicalPath).Returns(viewPath);
fileInfo.Setup(f => f.CreateReadStream()).Returns(Stream.Null);
// Act
razorService.CompileCore(fileInfo.Object).Wait();
// Assert
host.Verify();
}
}
}

View File

@ -0,0 +1,15 @@
{
"version" : "0.1-alpha-*",
"dependencies": {
"Microsoft.AspNet.FileSystems": "0.1-alpha-*",
"Microsoft.AspNet.Razor": "0.1-alpha-*",
"Microsoft.AspNet.Mvc.Razor" : "",
"Microsoft.AspNet.Mvc.Razor.Host" : "",
"Moq": "4.0.10827",
"Xunit": "1.9.1",
"Xunit.extensions": "1.9.1"
},
"configurations": {
"net45": { }
}
}

View File

@ -0,0 +1,10 @@
{
"shared": "*.cs",
"dependencies": {
"Xunit": "1.9.1",
"Xunit.extensions": "1.9.1"
},
"configurations": {
"net45": { }
}
}