From a490abc6e836581a1b3b86f298b49f9f7fe5a1f7 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 5 Aug 2014 08:20:00 -0700 Subject: [PATCH] Adding support for inheriting chunks from _ViewStarts as part of host parsing. Fixes #881 --- .../Directives/ChunkHelper.cs | 83 +++++++++ .../Directives/ChunkInheritanceUtility.cs | 147 ++++++++++++++++ .../Directives/IChunkMerger.cs | 26 +++ .../Directives/InjectChunkMerger.cs | 61 +++++++ .../Directives/SetBaseTypeChunkMerger.cs | 62 +++++++ .../Directives/UsingChunkMerger.cs | 36 ++++ .../InjectChunk.cs | 14 +- .../MvcCSharpCodeBuilder.cs | 24 ++- .../MvcRazorHost.cs | 119 ++++++++----- .../MvcRazorHostOptions.cs | 42 ----- .../Properties/AssemblyInfo.cs | 6 + .../Properties/Resources.Designer.cs | 16 ++ .../Resources.resx | 3 + .../ViewStartUtility.cs | 89 ++++++++++ .../project.json | 4 +- src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs | 26 ++- .../ViewStartProvider.cs | 56 +----- src/Microsoft.AspNet.Mvc/MvcServices.cs | 3 +- .../DirectivesTest.cs | 50 ++++++ .../Directives/ChunkInheritanceUtilityTest.cs | 153 +++++++++++++++++ .../Directives/InjectChunkMergerTest.cs | 160 ++++++++++++++++++ .../Directives/SetBaseTypeChunkMergerTest.cs | 104 ++++++++++++ .../Directives/UsingChunkMergerTest.cs | 106 ++++++++++++ .../InjectChunkVisitorTest.cs | 26 +-- .../ModelChunkVisitorTest.cs | 8 +- .../TestFileSystem.cs | 45 +++++ .../TestFiles/Output/Inject.cs | 4 +- .../TestFiles/Output/InjectWithModel.cs | 6 +- .../TestFiles/Output/Model.cs | 4 +- .../ViewStartUtilityTest.cs} | 39 ++--- .../project.json | 3 +- .../Controllers/DirectivesController.cs | 22 +++ test/WebSites/RazorWebSite/MyBasePage.cs | 23 +++ .../RazorWebSite/Services/InjectedHelper.cs | 13 ++ test/WebSites/RazorWebSite/Startup.cs | 1 + .../ViewInheritsBasePageFromViewStarts.cshtml | 1 + .../Views/Directives/Scoped/_Layout.cshtml | 1 + .../Views/Directives/Scoped/_ViewStart.cshtml | 4 + ...heritsInjectAndUsingsFromViewStarts.cshtml | 2 + .../Views/Directives/_ViewStart.cshtml | 2 + .../RazorWebSite/Views/Shared/_Partial.cshtml | 2 +- .../RazorWebSite/Views/_ViewStart.cshtml | 1 + 42 files changed, 1388 insertions(+), 209 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkHelper.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/Directives/IChunkMerger.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/Directives/InjectChunkMerger.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/Directives/SetBaseTypeChunkMerger.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/Directives/UsingChunkMerger.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHostOptions.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/ViewStartUtility.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/DirectivesTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/InjectChunkMergerTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/SetBaseTypeChunkMergerTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/UsingChunkMergerTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileSystem.cs rename test/{Microsoft.AspNet.Mvc.Razor.Test/ViewStartProviderTest.cs => Microsoft.AspNet.Mvc.Razor.Host.Test/ViewStartUtilityTest.cs} (60%) create mode 100644 test/WebSites/RazorWebSite/Controllers/DirectivesController.cs create mode 100644 test/WebSites/RazorWebSite/MyBasePage.cs create mode 100644 test/WebSites/RazorWebSite/Services/InjectedHelper.cs create mode 100644 test/WebSites/RazorWebSite/Views/Directives/Scoped/ViewInheritsBasePageFromViewStarts.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/Directives/Scoped/_Layout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/Directives/Scoped/_ViewStart.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/Directives/ViewInheritsInjectAndUsingsFromViewStarts.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/Directives/_ViewStart.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/_ViewStart.cshtml diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkHelper.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkHelper.cs new file mode 100644 index 000000000..95879e921 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkHelper.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.AspNet.Mvc.Razor.Host; +using Microsoft.AspNet.Razor.Generator.Compiler; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// Contains helper methods for dealing with Chunks + /// + public static class ChunkHelper + { + private const string TModelToken = ""; + + /// + /// Attempts to cast the passed in to type and throws if the + /// cast fails. + /// + /// The type to cast to. + /// The chunk to cast. + /// The cast to . + /// is not an instance of + /// . + public static TChunk EnsureChunk([NotNull] Chunk chunk) + where TChunk : Chunk + { + var chunkOfT = chunk as TChunk; + if (chunkOfT == null) + { + var message = Resources.FormatArgumentMustBeOfType(typeof(TChunk).FullName); + throw new ArgumentException(message, "chunk"); + } + + return chunkOfT; + } + + /// + /// Returns the used to determine the model name for the page generated + /// using the specified + /// + /// The to scan for s in. + /// The last in the if found, null otherwise. + /// + public static ModelChunk GetModelChunk([NotNull] CodeTree codeTree) + { + // If there's more than 1 model chunk there will be a Razor error BUT we want intellisense to show up on + // the current model chunk that the user is typing. + return codeTree.Chunks + .OfType() + .LastOrDefault(); + } + + /// + /// Returns the type name of the Model specified via a in the + /// if specified or the default model type. + /// + /// The to scan for s in. + /// The name of the default model. + /// The model type name for the generated page. + public static string GetModelTypeName([NotNull] CodeTree codeTree, + [NotNull] string defaultModelName) + { + var modelChunk = GetModelChunk(codeTree); + return modelChunk != null ? modelChunk.ModelType : defaultModelName; + } + + /// + /// Returns a string with the <TModel> token replaced with the value specified in + /// . + /// + /// The string to replace the token in. + /// The model name to replace with. + /// A string with the token replaced. + public static string ReplaceTModel([NotNull] string value, + [NotNull] string modelName) + { + return value.Replace(TModelToken, modelName); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs new file mode 100644 index 000000000..20c24d57c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.AspNet.FileSystems; +using Microsoft.AspNet.Razor; +using Microsoft.AspNet.Razor.Generator.Compiler; +using Microsoft.AspNet.Razor.Parser; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// A utility type for supporting inheritance of chunks into a page from _ViewStart pages that apply to it. + /// + public class ChunkInheritanceUtility + { + private readonly IReadOnlyList _defaultInheritedChunks; + + /// + /// Instantiates a new instance of . + /// + /// The instance to add s to. + /// The list of s inherited by default. + /// The model type used in the event no model is specified via the + /// @model keyword. + public ChunkInheritanceUtility([NotNull] CodeTree codeTree, + [NotNull] IReadOnlyList defaultInheritedChunks, + [NotNull] string defaultModel) + { + CodeTree = codeTree; + _defaultInheritedChunks = defaultInheritedChunks; + ChunkMergers = GetMergerMappings(codeTree, defaultModel); + } + + /// + /// Gets the CodeTree to add inherited instances to. + /// + public CodeTree CodeTree { get; private set; } + + /// + /// Gets a dictionary mapping type to the used to merge + /// chunks of that type. + /// + public IDictionary ChunkMergers { get; private set; } + + /// + /// Gets the list of chunks that are to be inherited by a specified page. + /// Chunks are inherited from _ViewStarts that are applicable to the page. + /// + /// The used to parse _ViewStart pages. + /// The filesystem that represents the application. + /// The root of the application. + /// The path of the page to locate inherited chunks for. + /// A list of chunks that are applicable to the given page. + public List GetInheritedChunks([NotNull] MvcRazorHost razorHost, + [NotNull] IFileSystem fileSystem, + [NotNull] string appRoot, + [NotNull] string pagePath) + { + var inheritedChunks = new List(); + + var templateEngine = new RazorTemplateEngine(razorHost); + foreach (var viewStart in ViewStartUtility.GetViewStartLocations(appRoot, pagePath)) + { + IFileInfo fileInfo; + if (fileSystem.TryGetFileInfo(viewStart, out fileInfo)) + { + var parsedTree = ParseViewFile(templateEngine, fileInfo); + var chunksToAdd = parsedTree.Chunks + .Where(chunk => ChunkMergers.ContainsKey(chunk.GetType())); + inheritedChunks.AddRange(chunksToAdd); + } + } + + inheritedChunks.AddRange(_defaultInheritedChunks); + + return inheritedChunks; + } + + /// + /// Merges a list of chunks into the instance. + /// + /// The list of chunks to merge. + public void MergeInheritedChunks(List inherited) + { + var current = CodeTree.Chunks; + + // We merge chunks into the codeTree in two passes. In the first pass, we traverse the CodeTree visiting + // a mapped IChunkMerger for types that are registered. + foreach (var chunk in current) + { + if (ChunkMergers.TryGetValue(chunk.GetType(), out var merger)) + { + merger.VisitChunk(chunk); + } + } + + // In the second phase we invoke IChunkMerger.Merge for each chunk that has a mapped merger. + // During this phase, the merger can either add to the CodeTree or ignore the chunk based on the merging + // rules. + foreach (var chunk in inherited) + { + if (ChunkMergers.TryGetValue(chunk.GetType(), out var merger)) + { + // TODO: When mapping chunks, we should remove mapping information since it would be incorrect + // to generate it in the page that inherits it. Tracked by #945 + merger.Merge(CodeTree, chunk); + } + } + } + + private static Dictionary GetMergerMappings(CodeTree codeTree, string defaultModel) + { + var modelType = ChunkHelper.GetModelTypeName(codeTree, defaultModel); + return new Dictionary + { + { typeof(UsingChunk), new UsingChunkMerger() }, + { typeof(InjectChunk), new InjectChunkMerger(modelType) }, + { typeof(SetBaseTypeChunk), new SetBaseTypeChunkMerger(modelType) } + }; + } + + // TODO: This needs to be cached (#1016) + private CodeTree ParseViewFile(RazorTemplateEngine engine, + IFileInfo fileInfo) + { + using (var stream = fileInfo.CreateReadStream()) + { + using (var streamReader = new StreamReader(stream)) + { + var parseResults = engine.ParseTemplate(streamReader); + var className = ParserHelpers.SanitizeClassName(fileInfo.Name); + var language = engine.Host.CodeLanguage; + var codeGenerator = language.CreateCodeGenerator(className, + engine.Host.DefaultNamespace, + fileInfo.PhysicalPath, + engine.Host); + codeGenerator.Visit(parseResults); + return codeGenerator.Context.CodeTreeBuilder.CodeTree; + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/IChunkMerger.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/IChunkMerger.cs new file mode 100644 index 000000000..d0cc4d651 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/IChunkMerger.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Generator.Compiler; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// Defines the contract for merging instances from _ViewStart files. + /// + public interface IChunkMerger + { + /// + /// Visits a from the to merge into. + /// + /// A from the tree. + void VisitChunk(Chunk chunk); + + /// + /// Merges an inherited into the . + /// + /// The to merge into. + /// The to merge. + void Merge(CodeTree codeTree, Chunk chunk); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/InjectChunkMerger.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/InjectChunkMerger.cs new file mode 100644 index 000000000..5f93ca823 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/InjectChunkMerger.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Razor.Generator.Compiler; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// A that merges instances. + /// + public class InjectChunkMerger : IChunkMerger + { + private readonly HashSet _addedMemberNames = new HashSet(StringComparer.Ordinal); + private string _modelType; + + /// + /// Initializes a new instance of . + /// + /// The model type to be used to replace <TModel> tokens. + public InjectChunkMerger([NotNull] string modelType) + { + _modelType = '<' + modelType + '>'; + } + + /// + public void VisitChunk([NotNull] Chunk chunk) + { + var injectChunk = ChunkHelper.EnsureChunk(chunk); + injectChunk.TypeName = ChunkHelper.ReplaceTModel(injectChunk.TypeName, _modelType); + _addedMemberNames.Add(injectChunk.MemberName); + } + + /// + public void Merge([NotNull] CodeTree codeTree, [NotNull] Chunk chunk) + { + var injectChunk = ChunkHelper.EnsureChunk(chunk); + if (!_addedMemberNames.Contains(injectChunk.MemberName)) + { + _addedMemberNames.Add(injectChunk.MemberName); + codeTree.Chunks.Add(TransformChunk(injectChunk)); + } + } + + private InjectChunk TransformChunk(InjectChunk injectChunk) + { + var typeName = ChunkHelper.ReplaceTModel(injectChunk.TypeName, _modelType); + if (typeName != injectChunk.TypeName) + { + return new InjectChunk(typeName, injectChunk.MemberName) + { + Start = injectChunk.Start, + Association = injectChunk.Association + }; + } + return injectChunk; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/SetBaseTypeChunkMerger.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/SetBaseTypeChunkMerger.cs new file mode 100644 index 000000000..72723c886 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/SetBaseTypeChunkMerger.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Generator.Compiler; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// A that merges instances. + /// + public class SetBaseTypeChunkMerger : IChunkMerger + { + private readonly string _modelType; + private bool _isBaseTypeSet; + + /// + /// Initializes a new instance of . + /// + /// The type name of the model used by default. + public SetBaseTypeChunkMerger(string modelType) + { + _modelType = '<' + modelType + '>'; + } + + /// + public void VisitChunk([NotNull] Chunk chunk) + { + var setBaseTypeChunk = ChunkHelper.EnsureChunk(chunk); + setBaseTypeChunk.TypeName = ChunkHelper.ReplaceTModel(setBaseTypeChunk.TypeName, _modelType); + _isBaseTypeSet = true; + } + + /// + public void Merge([NotNull] CodeTree codeTree, [NotNull] Chunk chunk) + { + if (!_isBaseTypeSet) + { + var baseTypeChunk = ChunkHelper.EnsureChunk(chunk); + + // The base type can set exactly once and the first one we encounter wins. + _isBaseTypeSet = true; + + codeTree.Chunks.Add(TransformChunk(baseTypeChunk)); + } + } + + private SetBaseTypeChunk TransformChunk(SetBaseTypeChunk setBaseTypeChunk) + { + var typeName = ChunkHelper.ReplaceTModel(setBaseTypeChunk.TypeName, _modelType); + if (typeName != setBaseTypeChunk.TypeName) + { + return new SetBaseTypeChunk + { + TypeName = typeName, + Start = setBaseTypeChunk.Start, + Association = setBaseTypeChunk.Association + }; + } + return setBaseTypeChunk; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/UsingChunkMerger.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/UsingChunkMerger.cs new file mode 100644 index 000000000..091cbf04d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/UsingChunkMerger.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Generator.Compiler; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// A that merges instances. + /// + public class UsingChunkMerger : IChunkMerger + { + private readonly HashSet _currentUsings = new HashSet(StringComparer.Ordinal); + + /// + public void VisitChunk([NotNull] Chunk chunk) + { + var namespaceChunk = ChunkHelper.EnsureChunk(chunk); + _currentUsings.Add(namespaceChunk.Namespace); + } + + /// + public void Merge([NotNull] CodeTree codeTree, [NotNull] Chunk chunk) + { + var namespaceChunk = ChunkHelper.EnsureChunk(chunk); + + if (!_currentUsings.Contains(namespaceChunk.Namespace)) + { + _currentUsings.Add(namespaceChunk.Namespace); + codeTree.Chunks.Add(namespaceChunk); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunk.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunk.cs index c58b5f7a8..6300589a0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunk.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunk.cs @@ -10,8 +10,8 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// Represents the chunk for an @inject statement. /// - /// The type of object that would be injected - /// The member name the field is exposed to the page as. + /// The type name of the property to be injected + /// The member name of the property to be injected. public InjectChunk(string typeName, string propertyName) { @@ -19,8 +19,14 @@ namespace Microsoft.AspNet.Mvc.Razor MemberName = propertyName; } - public string TypeName { get; private set; } + /// + /// Gets or sets the type name of the property to be injected. + /// + public string TypeName { get; set; } - public string MemberName { get; private set; } + /// + /// Gets or sets the name of the property to be injected. + /// + public string MemberName { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs index 2c1cbe5e7..87b6a6f90 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; +using Microsoft.AspNet.Mvc.Razor.Directives; using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Generator.Compiler.CSharp; @@ -13,13 +11,16 @@ namespace Microsoft.AspNet.Mvc.Razor { public class MvcCSharpCodeBuilder : CSharpCodeBuilder { - private readonly MvcRazorHostOptions _hostOptions; + private readonly string _defaultModel; + private readonly string _activateAttribute; - public MvcCSharpCodeBuilder([NotNull] CodeGeneratorContext context, - [NotNull] MvcRazorHostOptions hostOptions) + public MvcCSharpCodeBuilder([NotNull] CodeGeneratorContext context, + [NotNull] string defaultModel, + [NotNull] string activateAttribute) : base(context) { - _hostOptions = hostOptions; + _defaultModel = defaultModel; + _activateAttribute = activateAttribute; } private string Model { get; set; } @@ -27,12 +28,9 @@ namespace Microsoft.AspNet.Mvc.Razor protected override CSharpCodeWritingScope BuildClassDeclaration(CSharpCodeWriter writer) { // Grab the last model chunk so it gets intellisense. - // NOTE: If there's more than 1 model chunk there will be a Razor error BUT we want intellisense to - // show up on the current model chunk that the user is typing. - var modelChunk = Context.CodeTreeBuilder.CodeTree.Chunks.OfType() - .LastOrDefault(); + var modelChunk = ChunkHelper.GetModelChunk(Context.CodeTreeBuilder.CodeTree); - Model = modelChunk != null ? modelChunk.ModelType : _hostOptions.DefaultModel; + Model = modelChunk != null ? modelChunk.ModelType : _defaultModel; // If there were any model chunks then we need to modify the class declaration signature. if (modelChunk != null) @@ -62,7 +60,7 @@ namespace Microsoft.AspNet.Mvc.Razor writer.WriteLineHiddenDirective(); - var injectVisitor = new InjectChunkVisitor(writer, Context, _hostOptions.ActivateAttributeName); + var injectVisitor = new InjectChunkVisitor(writer, Context, _activateAttribute); injectVisitor.Accept(Context.CodeTreeBuilder.CodeTree.Chunks); writer.WriteLine(); diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs index 60a96ab33..522b4b687 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs @@ -1,49 +1,71 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.IO; -using System.Linq; +using Microsoft.AspNet.FileSystems; +using Microsoft.AspNet.Mvc.Razor.Directives; using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Parser; +using Microsoft.Framework.Runtime; namespace Microsoft.AspNet.Mvc.Razor { public class MvcRazorHost : RazorEngineHost, IMvcRazorHost { - private const string ViewNamespace = "ASP"; - - private static readonly string[] _defaultNamespaces = new[] - { + private const string BaseType = "Microsoft.AspNet.Mvc.Razor.RazorPage"; + private static readonly string[] _defaultNamespaces = new[] + { "System", "System.Linq", "System.Collections.Generic", "Microsoft.AspNet.Mvc", "Microsoft.AspNet.Mvc.Rendering", }; + private static readonly Chunk[] _defaultInheritedChunks = new[] + { + new InjectChunk("Microsoft.AspNet.Mvc.Rendering.IHtmlHelper", "Html"), + new InjectChunk("Microsoft.AspNet.Mvc.IViewComponentHelper", "Component"), + new InjectChunk("Microsoft.AspNet.Mvc.IUrlHelper", "Url"), + }; - private readonly MvcRazorHostOptions _hostOptions; - + private readonly string _appRoot; + private readonly IFileSystem _fileSystem; // CodeGenerationContext.DefaultBaseClass is set to MyBaseType. // This field holds the type name without the generic decoration (MyBaseType) private readonly string _baseType; - public MvcRazorHost(Type baseType) - : this(baseType.FullName) + /// + /// Initializes a new instance of with the specified + /// . + /// + /// Contains information about the executing application. + public MvcRazorHost(IApplicationEnvironment appEnvironment) + : this(appEnvironment.ApplicationBasePath, + new PhysicalFileSystem(appEnvironment.ApplicationBasePath)) { } - public MvcRazorHost(string baseType) + /// + /// Initializes a new instance of at the specified application root + /// and . + /// + /// The base path of the application. + /// + /// A rooted at the . + /// + protected internal MvcRazorHost(string applicationBasePath, + IFileSystem fileSystem) : base(new CSharpRazorCodeLanguage()) { - // TODO: this needs to flow from the application rather than being initialized here. - // Tracked by #774 - _hostOptions = new MvcRazorHostOptions(); - _baseType = baseType; - DefaultBaseClass = baseType + '<' + _hostOptions.DefaultModel + '>'; + _appRoot = applicationBasePath; + _fileSystem = fileSystem; + _baseType = BaseType; + + DefaultBaseClass = BaseType + '<' + DefaultModel + '>'; + DefaultNamespace = "Asp"; GeneratedClassContext = new GeneratedClassContext( executeMethodName: "ExecuteAsync", writeMethodName: "Write", @@ -62,51 +84,64 @@ namespace Microsoft.AspNet.Mvc.Razor } } + /// + /// Gets the model type used by default when no model is specified. + /// + /// This value is used as the generic type argument for the base type + public virtual string DefaultModel + { + get { return "dynamic"; } + } + + /// + /// Gets the list of chunks that are injected by default by this host. + /// + public virtual IReadOnlyList DefaultInheritedChunks + { + get { return _defaultInheritedChunks; } + } + + /// + /// Gets or sets the name attribute that is used to decorate properties that are injected and need to be + /// activated. + /// + public virtual string ActivateAttribute + { + get { return "Microsoft.AspNet.Mvc.ActivateAttribute"; } + } + + /// public GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream) { var className = ParserHelpers.SanitizeClassName(rootRelativePath); using (var reader = new StreamReader(inputStream)) { var engine = new RazorTemplateEngine(this); - return engine.GenerateCode(reader, className, ViewNamespace, rootRelativePath); + return engine.GenerateCode(reader, className, DefaultNamespace, rootRelativePath); } } - public override ParserBase DecorateCodeParser(ParserBase incomingCodeParser) + /// + public override ParserBase DecorateCodeParser([NotNull] ParserBase incomingCodeParser) { return new MvcRazorCodeParser(_baseType); } - public override CodeBuilder DecorateCodeBuilder(CodeBuilder incomingBuilder, CodeGeneratorContext context) + /// + public override CodeBuilder DecorateCodeBuilder([NotNull] CodeBuilder incomingBuilder, + [NotNull] CodeGeneratorContext context) { UpdateCodeBuilder(context); - return new MvcCSharpCodeBuilder(context, _hostOptions); + return new MvcCSharpCodeBuilder(context, DefaultModel, ActivateAttribute); } private void UpdateCodeBuilder(CodeGeneratorContext context) { - var currentChunks = context.CodeTreeBuilder.CodeTree.Chunks; - var existingInjects = new HashSet(currentChunks.OfType() - .Select(c => c.MemberName), - StringComparer.Ordinal); - - var modelChunk = currentChunks.OfType() - .LastOrDefault(); - var model = _hostOptions.DefaultModel; - if (modelChunk != null) - { - model = modelChunk.ModelType; - } - model = '<' + model + '>'; - - // Locate properties by name that haven't already been injected in to the View. - var propertiesToAdd = _hostOptions.DefaultInjectedProperties - .Where(c => !existingInjects.Contains(c.MemberName)); - foreach (var property in propertiesToAdd) - { - var typeName = property.TypeName.Replace("", model); - currentChunks.Add(new InjectChunk(typeName, property.MemberName)); - } + var chunkUtility = new ChunkInheritanceUtility(context.CodeTreeBuilder.CodeTree, + DefaultInheritedChunks, + DefaultModel); + var inheritedChunks = chunkUtility.GetInheritedChunks(this, _fileSystem, _appRoot, context.SourceFile); + chunkUtility.MergeInheritedChunks(inheritedChunks); } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHostOptions.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHostOptions.cs deleted file mode 100644 index b95000e62..000000000 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHostOptions.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Mvc.Razor -{ - /// - /// Represents configuration options for the Razor Host - /// - public class MvcRazorHostOptions - { - public MvcRazorHostOptions() - { - DefaultModel = "dynamic"; - ActivateAttributeName = "Microsoft.AspNet.Mvc.ActivateAttribute"; - DefaultInjectedProperties = new List() - { - new InjectDescriptor("Microsoft.AspNet.Mvc.Rendering.IHtmlHelper", "Html"), - new InjectDescriptor("Microsoft.AspNet.Mvc.IViewComponentHelper", "Component"), - new InjectDescriptor("Microsoft.AspNet.Mvc.IUrlHelper", "Url"), - }; - } - - /// - /// Gets or sets the model that is used by default for generated views - /// when no model is explicily specified. Defaults to dynamic. - /// - public string DefaultModel { get; set; } - - /// - /// Gets or sets the attribue that is used to decorate properties that are injected and need to - /// be activated. - /// - public string ActivateAttributeName { get; set; } - - /// - /// Gets the list of properties that are injected by default. - /// - public IList DefaultInjectedProperties { get; private set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..d6cecb56a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Razor.Host.Test")] diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs index 4f2663c2b..b1aa28a73 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs @@ -26,6 +26,22 @@ namespace Microsoft.AspNet.Mvc.Razor.Host return GetString("ArgumentCannotBeNullOrEmpy"); } + /// + /// Argument must be an instance of type '{0}'. + /// + internal static string ArgumentMustBeOfType + { + get { return GetString("ArgumentMustBeOfType"); } + } + + /// + /// Argument must be an instance of type '{0}'. + /// + internal static string FormatArgumentMustBeOfType(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ArgumentMustBeOfType"), p0); + } + /// /// The 'inherits' keyword is not allowed when a '{0}' keyword is used. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx index cfa565507..460a42f1c 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx @@ -120,6 +120,9 @@ Argument cannot be null or empty. + + Argument must be an instance of '{0}'. + The 'inherits' keyword is not allowed when a '{0}' keyword is used. diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/ViewStartUtility.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/ViewStartUtility.cs new file mode 100644 index 000000000..4e2dda029 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/ViewStartUtility.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public static class ViewStartUtility + { + private const string ViewStartFileName = "_viewstart.cshtml"; + + /// + /// Determines if the given path represents a view start file. + /// + /// The path to inspect. + /// True if the path is a view start file, false otherwise. + public static bool IsViewStart([NotNull] string path) + { + var fileName = Path.GetFileName(path); + return string.Equals(ViewStartFileName, fileName, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Gets the view start locations that are applicable to the specified path. + /// + /// The base of the application. + /// The path to locate view starts for. + /// A sequence of paths that represent potential view start locations. + /// + /// This method returns paths starting from the directory of and moves + /// upwards until it hits the application root. + /// e.g. + /// /Views/Home/View.cshtml -> [ /Views/Home/_ViewStart.cshtml, /Views/_ViewStart.cshtml, /_ViewStart.cshtml ] + /// + public static IEnumerable GetViewStartLocations(string applicationBase, string path) + { + if (string.IsNullOrEmpty(path)) + { + return Enumerable.Empty(); + } + + applicationBase = TrimTrailingSlash(applicationBase); + var viewStartLocations = new List(); + var currentDir = GetViewDirectory(applicationBase, path); + while (IsSubDirectory(applicationBase, currentDir)) + { + viewStartLocations.Add(Path.Combine(currentDir, ViewStartFileName)); + currentDir = Path.GetDirectoryName(currentDir); + } + + return viewStartLocations; + } + + private static bool IsSubDirectory(string appRoot, string currentDir) + { + return currentDir.StartsWith(appRoot, StringComparison.OrdinalIgnoreCase); + } + + private static string GetViewDirectory(string appRoot, string viewPath) + { + if (viewPath.StartsWith("~/")) + { + viewPath = viewPath.Substring(2); + } + else if (viewPath[0] == Path.DirectorySeparatorChar || + viewPath[0] == Path.AltDirectorySeparatorChar) + { + viewPath = viewPath.Substring(1); + } + + var viewDir = Path.GetDirectoryName(viewPath); + return Path.GetFullPath(Path.Combine(appRoot, viewDir)); + } + + private static string TrimTrailingSlash(string path) + { + if (path.Length > 0 && + path[path.Length - 1] == Path.DirectorySeparatorChar) + { + return path.Substring(0, path.Length - 1); + } + + return path; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/project.json b/src/Microsoft.AspNet.Mvc.Razor.Host/project.json index a687c520d..7961146f7 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/project.json +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/project.json @@ -4,8 +4,10 @@ "warningsAsErrors": true }, "dependencies": { + "Microsoft.AspNet.FileSystems": "1.0.0-*", "Microsoft.AspNet.Mvc.Common": "", - "Microsoft.AspNet.Razor": "4.0.0-*" + "Microsoft.AspNet.Razor": "4.0.0-*", + "Microsoft.Framework.Runtime.Interfaces": "1.0.0-*" }, "frameworks": { "net45": { diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index 73490d474..2c3c55990 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -99,11 +99,26 @@ namespace Microsoft.AspNet.Mvc.Razor /// public abstract Task ExecuteAsync(); + /// + /// Writes the specified with HTML encoding to . + /// + /// The to write. public virtual void Write(object value) { WriteTo(Output, value); } + /// + /// Writes the specified with HTML encoding to . + /// + /// The instance to write to. + /// The to write. + /// + /// s of type are written without encoding and the + /// is invoked for types. + /// For all other types, the encoded result of is written to the + /// . + /// public virtual void WriteTo(TextWriter writer, object content) { if (content != null) @@ -128,11 +143,20 @@ namespace Microsoft.AspNet.Mvc.Razor } } - public void WriteLiteral(object value) + /// + /// Writes the specified without HTML encoding to . + /// + /// The to write. + public virtual void WriteLiteral(object value) { WriteLiteralTo(Output, value); } + /// + /// Writes the specified without HTML encoding to the . + /// + /// The instance to write to. + /// The to write. public virtual void WriteLiteralTo(TextWriter writer, object text) { if (text != null) diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs index 2f43fd10e..215cce468 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using Microsoft.Framework.Runtime; @@ -12,21 +11,20 @@ namespace Microsoft.AspNet.Mvc.Razor /// public class ViewStartProvider : IViewStartProvider { - private const string ViewStartFileName = "_ViewStart.cshtml"; private readonly string _appRoot; private readonly IRazorPageFactory _pageFactory; public ViewStartProvider(IApplicationEnvironment appEnv, IRazorPageFactory pageFactory) { - _appRoot = TrimTrailingSlash(appEnv.ApplicationBasePath); + _appRoot = appEnv.ApplicationBasePath; _pageFactory = pageFactory; } /// public IEnumerable GetViewStartPages([NotNull] string path) { - var viewStartLocations = GetViewStartLocations(path); + var viewStartLocations = ViewStartUtility.GetViewStartLocations(_appRoot, path); var viewStarts = viewStartLocations.Select(_pageFactory.CreateInstance) .Where(p => p != null) .ToArray(); @@ -38,55 +36,5 @@ namespace Microsoft.AspNet.Mvc.Razor return viewStarts; } - - internal IEnumerable GetViewStartLocations(string path) - { - if (string.IsNullOrEmpty(path)) - { - return Enumerable.Empty(); - } - - var viewStartLocations = new List(); - var currentDir = GetViewDirectory(_appRoot, path); - while (IsSubDirectory(_appRoot, currentDir)) - { - viewStartLocations.Add(Path.Combine(currentDir, ViewStartFileName)); - currentDir = Path.GetDirectoryName(currentDir); - } - - return viewStartLocations; - } - - private static bool IsSubDirectory(string appRoot, string currentDir) - { - return currentDir.StartsWith(appRoot, StringComparison.OrdinalIgnoreCase); - } - - private static string GetViewDirectory(string appRoot, string viewPath) - { - if (viewPath.StartsWith("~/")) - { - viewPath = viewPath.Substring(2); - } - else if (viewPath[0] == Path.DirectorySeparatorChar || - viewPath[0] == Path.AltDirectorySeparatorChar) - { - viewPath = viewPath.Substring(1); - } - - var viewDir = Path.GetDirectoryName(viewPath); - return Path.GetFullPath(Path.Combine(appRoot, viewDir)); - } - - private static string TrimTrailingSlash(string path) - { - if (path.Length > 0 && - path[path.Length - 1] == Path.DirectorySeparatorChar) - { - return path.Substring(0, path.Length - 1); - } - - return path; - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 195f331a5..beeb20bc6 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -42,7 +42,8 @@ namespace Microsoft.AspNet.Mvc yield return describe.Transient(); yield return describe.Transient(); - yield return describe.Instance(new MvcRazorHost(typeof(RazorPage).FullName)); + // The host is designed to be discarded after consumption and is very inexpensive to initialize. + yield return describe.Transient(); yield return describe.Singleton(); yield return describe.Singleton(); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/DirectivesTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/DirectivesTest.cs new file mode 100644 index 000000000..3b4c20a56 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/DirectivesTest.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using RazorWebSite; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class DirectivesTest + { + private readonly IServiceProvider _provider = TestHelper.CreateServices("RazorWebSite"); + private readonly Action _app = new Startup().Configure; + + [Fact] + public async Task ViewsInheritsUsingsAndInjectDirectivesFromViewStarts() + { + var expected = @"Hello Person1"; + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + + // Act + var result = await client.GetAsync("http://localhost/Directives/ViewInheritsInjectAndUsingsFromViewStarts"); + + // Assert + var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); + Assert.Equal(expected, body.Trim()); + } + + [Fact] + public async Task ViewInheritsBasePageFromViewStarts() + { + var expected = @"WriteLiteral says:layout:Write says:Write says:Hello Person2"; + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + + // Act + var result = await client.GetAsync("http://localhost/Directives/ViewInheritsBasePageFromViewStarts"); + + // Assert + var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); + Assert.Equal(expected, body.Trim()); + } + + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs new file mode 100644 index 000000000..38f7e1982 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Generator.Compiler; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + public class ChunkInheritanceUtilityTest + { + [Fact] + public void GetInheritedChunks_ReadsChunksFromViewStartsInPath() + { + // Arrange + var appRoot = @"x:\myapproot"; + var fileSystem = new TestFileSystem(); + fileSystem.AddFile(@"x:\myapproot\views\accounts\_viewstart.cshtml", "@using AccountModels"); + fileSystem.AddFile(@"x:\myapproot\views\Shared\_viewstart.cshtml", "@inject SharedHelper Shared"); + fileSystem.AddFile(@"x:\myapproot\views\home\_viewstart.cshtml", "@using MyNamespace"); + fileSystem.AddFile(@"x:\myapproot\views\_viewstart.cshtml", +@"@inject MyHelper Helper +@inherits MyBaseType + +@{ + Layout = ""test.cshtml""; +} + +"); + var host = new MvcRazorHost(appRoot, fileSystem); + var utility = new ChunkInheritanceUtility(new CodeTree(), new Chunk[0], "dynamic"); + + // Act + var chunks = utility.GetInheritedChunks(host, + fileSystem, + appRoot, + @"x:\myapproot\views\home\Index.cshtml"); + + // Assert + Assert.Equal(3, chunks.Count); + var usingChunk = Assert.IsType(chunks[0]); + Assert.Equal("MyNamespace", usingChunk.Namespace); + + var injectChunk = Assert.IsType(chunks[1]); + Assert.Equal("MyHelper", injectChunk.TypeName); + Assert.Equal("Helper", injectChunk.MemberName); + + var setBaseTypeChunk = Assert.IsType(chunks[2]); + Assert.Equal("MyBaseType", setBaseTypeChunk.TypeName); + } + + [Fact] + public void GetInheritedChunks_ReturnsEmptySequenceIfNoViewStartsArePresent() + { + // Arrange + var appRoot = @"x:\myapproot"; + var fileSystem = new TestFileSystem(); + fileSystem.AddFile(@"x:\myapproot\_viewstart.cs", string.Empty); + fileSystem.AddFile(@"x:\myapproot\views\_Layout.cshtml", string.Empty); + fileSystem.AddFile(@"x:\myapproot\views\home\_not-viewstart.cshtml", string.Empty); + var host = new MvcRazorHost(appRoot, fileSystem); + var utility = new ChunkInheritanceUtility(new CodeTree(), new Chunk[0], "dynamic"); + + // Act + var chunks = utility.GetInheritedChunks(host, + fileSystem, + appRoot, + @"x:\myapproot\views\home\Index.cshtml"); + + // Assert + Assert.Empty(chunks); + } + + [Fact] + public void GetInheritedChunks_ReturnsDefaultInheritedChunks() + { + // Arrange + var appRoot = @"x:\myapproot"; + var fileSystem = new TestFileSystem(); + fileSystem.AddFile(@"x:\myapproot\views\_viewstart.cshtml", +@"@inject DifferentHelper Html +@using AppNamespace.Models +@{ + Layout = ""test.cshtml""; +} + +"); + var host = new MvcRazorHost(appRoot, fileSystem); + var defaultChunks = new Chunk[] + { + new InjectChunk("MyTestHtmlHelper", "Html"), + new UsingChunk { Namespace = "AppNamespace.Model" }, + }; + var utility = new ChunkInheritanceUtility(new CodeTree(), defaultChunks, "dynamic"); + + // Act + var chunks = utility.GetInheritedChunks(host, + fileSystem, + appRoot, + @"x:\myapproot\views\home\Index.cshtml"); + + // Assert + Assert.Equal(4, chunks.Count); + var injectChunk = Assert.IsType(chunks[0]); + Assert.Equal("DifferentHelper", injectChunk.TypeName); + Assert.Equal("Html", injectChunk.MemberName); + + var usingChunk = Assert.IsType(chunks[1]); + Assert.Equal("AppNamespace.Models", usingChunk.Namespace); + + injectChunk = Assert.IsType(chunks[2]); + Assert.Equal("MyTestHtmlHelper", injectChunk.TypeName); + Assert.Equal("Html", injectChunk.MemberName); + + usingChunk = Assert.IsType(chunks[3]); + Assert.Equal("AppNamespace.Model", usingChunk.Namespace); + } + + [Fact] + public void MergeChunks_VisitsChunksPriorToMerging() + { + // Arrange + var codeTree = new CodeTree(); + codeTree.Chunks.Add(new LiteralChunk()); + codeTree.Chunks.Add(new ExpressionBlockChunk()); + codeTree.Chunks.Add(new ExpressionBlockChunk()); + + var merger = new Mock(); + var mockSequence = new MockSequence(); + merger.InSequence(mockSequence) + .Setup(m => m.VisitChunk(It.IsAny())) + .Verifiable(); + merger.InSequence(mockSequence) + .Setup(m => m.Merge(codeTree, It.IsAny())) + .Verifiable(); + var inheritedChunks = new List + { + new CodeAttributeChunk(), + new LiteralChunk() + }; + var utility = new ChunkInheritanceUtility(codeTree, inheritedChunks, "dynamic"); + + // Act + utility.ChunkMergers[typeof(LiteralChunk)] = merger.Object; + utility.MergeInheritedChunks(inheritedChunks); + + // Assert + merger.Verify(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/InjectChunkMergerTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/InjectChunkMergerTest.cs new file mode 100644 index 000000000..a7a4a14ea --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/InjectChunkMergerTest.cs @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Generator.Compiler; +using Microsoft.AspNet.Testing; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + public class InjectChunkMergerTest + { + [Fact] + public void Visit_ThrowsIfThePassedInChunkIsNotAInjectChunk() + { + // Arrange + var expected = "Argument must be an instance of 'Microsoft.AspNet.Mvc.Razor.InjectChunk'."; + var merger = new InjectChunkMerger("dynamic"); + + // Act and Assert + ExceptionAssert.ThrowsArgument(() => merger.VisitChunk(new LiteralChunk()), "chunk", expected); + } + + [Theory] + [InlineData("MyApp.TestHelper", "MyApp.TestHelper")] + [InlineData("TestBaseType", "TestBaseType")] + public void Visit_UpdatesTModelTokenToMatchModelType(string typeName, string expectedValue) + { + // Arrange + var chunk = new InjectChunk(typeName, "TestHelper"); + var merger = new InjectChunkMerger("Person"); + + // Act + merger.VisitChunk(chunk); + + // Assert + Assert.Equal(expectedValue, chunk.TypeName); + Assert.Equal("TestHelper", chunk.MemberName); + } + + [Fact] + public void Merge_ThrowsIfThePassedInChunkIsNotAInjectChunk() + { + // Arrange + var expected = "Argument must be an instance of 'Microsoft.AspNet.Mvc.Razor.InjectChunk'."; + var merger = new InjectChunkMerger("dynamic"); + + // Act and Assert + ExceptionAssert.ThrowsArgument(() => merger.Merge(new CodeTree(), new LiteralChunk()), "chunk", expected); + } + + [Fact] + public void Merge_AddsChunkIfChunkWithMatchingPropertyNameWasNotVisitedInCodeTree() + { + // Arrange + var expectedType = "MyApp.MyHelperType"; + var expectedProperty = "MyHelper"; + var merger = new InjectChunkMerger("dynamic"); + var codeTree = new CodeTree(); + + // Act + merger.Merge(codeTree, new InjectChunk(expectedType, expectedProperty)); + + // Assert + var chunk = Assert.Single(codeTree.Chunks); + var injectChunk = Assert.IsType(chunk); + Assert.Equal(expectedType, injectChunk.TypeName); + Assert.Equal(expectedProperty, injectChunk.MemberName); + } + + [Fact] + public void Merge_IgnoresChunkIfChunkWithMatchingPropertyNameWasVisitedInCodeTree() + { + // Arrange + var merger = new InjectChunkMerger("dynamic"); + var codeTree = new CodeTree(); + + // Act + merger.VisitChunk(new InjectChunk("MyTypeA", "MyProperty")); + merger.Merge(codeTree, new InjectChunk("MyTypeB", "MyProperty")); + + // Assert + Assert.Empty(codeTree.Chunks); + } + + [Fact] + public void Merge_MatchesPropertyNameInCaseSensitiveManner() + { + // Arrange + var merger = new InjectChunkMerger("dynamic"); + var codeTree = new CodeTree(); + + // Act + merger.VisitChunk(new InjectChunk("MyType", "MyProperty")); + merger.Merge(codeTree, new InjectChunk("MyType", "myproperty")); + merger.Merge(codeTree, new InjectChunk("MyTypeB", "different-property")); + + // Assert + Assert.Equal(2, codeTree.Chunks.Count); + var injectChunk = Assert.IsType(codeTree.Chunks[0]); + Assert.Equal("MyType", injectChunk.TypeName); + Assert.Equal("myproperty", injectChunk.MemberName); + + injectChunk = Assert.IsType(codeTree.Chunks[1]); + Assert.Equal("MyTypeB", injectChunk.TypeName); + Assert.Equal("different-property", injectChunk.MemberName); + } + + [Fact] + public void Merge_ResolvesModelNameInTypesWithTModelToken() + { + // Arrange + var merger = new InjectChunkMerger("dynamic"); + var codeTree = new CodeTree(); + + // Act + merger.Merge(codeTree, new InjectChunk("MyHelper", "MyProperty")); + + // Assert + var chunk = Assert.Single(codeTree.Chunks); + var injectChunk = Assert.IsType(chunk); + Assert.Equal("MyHelper", injectChunk.TypeName); + Assert.Equal("MyProperty", injectChunk.MemberName); + } + + [Fact] + public void Merge_ReplacesTModelTokensWithModel() + { + // Arrange + var merger = new InjectChunkMerger("MyTestModel2"); + var codeTree = new CodeTree(); + + // Act + merger.Merge(codeTree, new InjectChunk("MyHelper", "MyProperty")); + + // Assert + var chunk = Assert.Single(codeTree.Chunks); + var injectChunk = Assert.IsType(chunk); + Assert.Equal("MyHelper", injectChunk.TypeName); + Assert.Equal("MyProperty", injectChunk.MemberName); + } + + [Fact] + public void Merge_IgnoresChunkIfChunkWithMatchingPropertyNameWasPreviouslyMerged() + { + // Arrange + var merger = new InjectChunkMerger("dynamic"); + var codeTree = new CodeTree(); + + // Act + merger.Merge(codeTree, new InjectChunk("SomeType", "Property")); + merger.Merge(codeTree, new InjectChunk("SomeOtherType", "Property")); + + // Assert + var chunk = Assert.Single(codeTree.Chunks); + var injectChunk = Assert.IsType(chunk); + Assert.Equal("SomeType", injectChunk.TypeName); + Assert.Equal("Property", injectChunk.MemberName); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/SetBaseTypeChunkMergerTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/SetBaseTypeChunkMergerTest.cs new file mode 100644 index 000000000..62a3dd5b0 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/SetBaseTypeChunkMergerTest.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Generator.Compiler; +using Microsoft.AspNet.Testing; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + public class SetBaseTypeChunkMergerTest + { + [Fact] + public void Visit_ThrowsIfThePassedInChunkIsNotASetBaseTypeChunk() + { + // Arrange + var expected = "Argument must be an instance of "+ + "'Microsoft.AspNet.Razor.Generator.Compiler.SetBaseTypeChunk'."; + var merger = new SetBaseTypeChunkMerger("dynamic"); + + // Act and Assert + ExceptionAssert.ThrowsArgument(() => merger.VisitChunk(new LiteralChunk()), "chunk", expected); + } + + [Theory] + [InlineData("MyApp.BaseType", "MyApp.BaseType")] + [InlineData("TestBaseType", "TestBaseType")] + public void Visit_UpdatesTModelTokenToMatchModelType(string typeName, string expectedValue) + { + // Arrange + var chunk = new SetBaseTypeChunk + { + TypeName = typeName, + }; + var merger = new SetBaseTypeChunkMerger("Person"); + + // Act + merger.VisitChunk(chunk); + + // Assert + Assert.Equal(expectedValue, chunk.TypeName); + } + + [Fact] + public void Merge_ThrowsIfThePassedInChunkIsNotASetBaseTypeChunk() + { + // Arrange + var expected = "Argument must be an instance of " + + "'Microsoft.AspNet.Razor.Generator.Compiler.SetBaseTypeChunk'."; + var merger = new SetBaseTypeChunkMerger("dynamic"); + + // Act and Assert + ExceptionAssert.ThrowsArgument(() => merger.Merge(new CodeTree(), new LiteralChunk()), "chunk", expected); + } + + [Fact] + public void Merge_SetsBaseTypeIfItHasNotBeenSetInCodeTree() + { + // Arrange + var expected = "MyApp.Razor.MyBaseType"; + var merger = new SetBaseTypeChunkMerger("dynamic"); + var codeTree = new CodeTree(); + + // Act + merger.Merge(codeTree, new SetBaseTypeChunk { TypeName = expected }); + + // Assert + var chunk = Assert.Single(codeTree.Chunks); + var setBaseTypeChunk = Assert.IsType(chunk); + Assert.Equal(expected, setBaseTypeChunk.TypeName); + } + + [Fact] + public void Merge_IgnoresSetBaseTypeChunksIfCodeTreeContainsOne() + { + // Arrange + var merger = new SetBaseTypeChunkMerger("dynamic"); + var codeTree = new CodeTree(); + + // Act + merger.VisitChunk(new SetBaseTypeChunk { TypeName = "MyBaseType1" }); + merger.Merge(codeTree, new SetBaseTypeChunk { TypeName = "MyBaseType2" }); + + // Assert + Assert.Empty(codeTree.Chunks); + } + + [Fact] + public void Merge_IgnoresSetBaseTypeChunksIfSetBaseTypeWasPreviouslyMerged() + { + // Arrange + var merger = new SetBaseTypeChunkMerger("dynamic"); + var codeTree = new CodeTree(); + + // Act + merger.Merge(codeTree, new SetBaseTypeChunk { TypeName = "MyBase1" }); + merger.Merge(codeTree, new SetBaseTypeChunk { TypeName = "MyBase2" }); + + // Assert + var chunk = Assert.Single(codeTree.Chunks); + var setBaseTypeChunk = Assert.IsType(chunk); + Assert.Equal("MyBase1", setBaseTypeChunk.TypeName); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/UsingChunkMergerTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/UsingChunkMergerTest.cs new file mode 100644 index 000000000..2aa51c0c8 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/UsingChunkMergerTest.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Generator.Compiler; +using Microsoft.AspNet.Testing; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + public class UsingChunkMergerTest + { + [Fact] + public void Visit_ThrowsIfThePassedInChunkIsNotAUsingChunk() + { + // Arrange + var expected = "Argument must be an instance of 'Microsoft.AspNet.Razor.Generator.Compiler.UsingChunk'."; + var merger = new UsingChunkMerger(); + + // Act and Assert + ExceptionAssert.ThrowsArgument(() => merger.VisitChunk(new LiteralChunk()), "chunk", expected); + } + + [Fact] + public void Merge_ThrowsIfThePassedInChunkIsNotAUsingChunk() + { + // Arrange + var expected = "Argument must be an instance of 'Microsoft.AspNet.Razor.Generator.Compiler.UsingChunk'."; + var merger = new UsingChunkMerger(); + + // Act and Assert + ExceptionAssert.ThrowsArgument(() => merger.Merge(new CodeTree(), new LiteralChunk()), "chunk", expected); + } + + [Fact] + public void Merge_AddsNamespacesThatHaveNotBeenVisitedInCodeTree() + { + // Arrange + var expected = "MyApp.Models"; + var merger = new UsingChunkMerger(); + var codeTree = new CodeTree(); + + // Act + merger.VisitChunk(new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" }); + merger.Merge(codeTree, new UsingChunk { Namespace = expected }); + + // Assert + var chunk = Assert.Single(codeTree.Chunks); + var usingChunk = Assert.IsType(chunk); + Assert.Equal(expected, usingChunk.Namespace); + } + + [Fact] + public void Merge_IgnoresNamespacesThatHaveBeenVisitedInCodeTree() + { + // Arrange + var merger = new UsingChunkMerger(); + var codeTree = new CodeTree(); + + // Act + merger.VisitChunk(new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" }); + merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" }); + + // Assert + Assert.Empty(codeTree.Chunks); + } + + [Fact] + public void Merge_IgnoresNamespacesThatHaveBeenVisitedDuringMerge() + { + // Arrange + var merger = new UsingChunkMerger(); + var codeTree = new CodeTree(); + + // Act + merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" }); + merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" }); + merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.Mvc.Razor" }); + + // Assert + Assert.Equal(2, codeTree.Chunks.Count); + var chunk = Assert.IsType(codeTree.Chunks[0]); + Assert.Equal("Microsoft.AspNet.Mvc", chunk.Namespace); + chunk = Assert.IsType(codeTree.Chunks[1]); + Assert.Equal("Microsoft.AspNet.Mvc.Razor", chunk.Namespace); + } + + [Fact] + public void Merge_MacthesNamespacesInCaseSensitiveManner() + { + // Arrange + var merger = new UsingChunkMerger(); + var codeTree = new CodeTree(); + + // Act + merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" }); + merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.mvc" }); + + // Assert + Assert.Equal(2, codeTree.Chunks.Count); + var chunk = Assert.IsType(codeTree.Chunks[0]); + Assert.Equal("Microsoft.AspNet.Mvc", chunk.Namespace); + chunk = Assert.IsType(codeTree.Chunks[1]); + Assert.Equal("Microsoft.AspNet.mvc", chunk.Namespace); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs index 718069b57..69632a5b5 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.IO; +using Microsoft.AspNet.FileSystems; using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Generator.Compiler.CSharp; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Text; +using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.Razor @@ -114,7 +116,7 @@ MyType2 @MyPropertyName2 public void InjectVisitor_GeneratesCorrectLineMappings() { // Arrange - var host = new MvcRazorHost("RazorView") + var host = new MvcRazorHost("appRoot", Mock.Of()) { DesignTimeMode = true }; @@ -124,8 +126,8 @@ MyType2 @MyPropertyName2 var expectedCode = ReadResource("TestFiles/Output/Inject.cs"); var expectedLineMappings = new List { - BuildLineMapping(1, 0, 1, 32, 3, 0, 17), - BuildLineMapping(28, 1, 8, 573, 26, 8, 20) + BuildLineMapping(1, 0, 1, 30, 3, 0, 17), + BuildLineMapping(28, 1, 8, 598, 26, 8, 20) }; // Act @@ -146,7 +148,7 @@ MyType2 @MyPropertyName2 public void InjectVisitorWithModel_GeneratesCorrectLineMappings() { // Arrange - var host = new MvcRazorHost("RazorView") + var host = new MvcRazorHost("appRoot", Mock.Of()) { DesignTimeMode = true }; @@ -156,9 +158,9 @@ MyType2 @MyPropertyName2 var expectedCode = ReadResource("TestFiles/Output/InjectWithModel.cs"); var expectedLineMappings = new List { - BuildLineMapping(7, 0, 7, 126, 6, 7, 7), - BuildLineMapping(24, 1, 8, 562, 26, 8, 20), - BuildLineMapping(54, 2, 8, 732, 34, 8, 22) + BuildLineMapping(7, 0, 7, 151, 6, 7, 7), + BuildLineMapping(24, 1, 8, 587, 26, 8, 20), + BuildLineMapping(54, 2, 8, 757, 34, 8, 23) }; // Act @@ -188,7 +190,7 @@ MyType2 @MyPropertyName2 private static CodeGeneratorContext CreateContext() { - return CodeGeneratorContext.Create(new MvcRazorHost("RazorView"), + return CodeGeneratorContext.Create(new MvcRazorHost("appRoot", Mock.Of()), "MyClass", "MyNamespace", string.Empty, @@ -203,11 +205,11 @@ MyType2 @MyPropertyName2 int generatedCharacterIndex, int contentLength) { - var documentLocation = new SourceLocation(documentAbsoluteIndex, - documentLineIndex, + var documentLocation = new SourceLocation(documentAbsoluteIndex, + documentLineIndex, documentCharacterIndex); - var generatedLocation = new SourceLocation(generatedAbsoluteIndex, - generatedLineIndex, + var generatedLocation = new SourceLocation(generatedAbsoluteIndex, + generatedLineIndex, generatedCharacterIndex); return new LineMapping( diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/ModelChunkVisitorTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/ModelChunkVisitorTest.cs index e1fe84484..02b8f1b64 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/ModelChunkVisitorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/ModelChunkVisitorTest.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; using System.IO; +using Microsoft.AspNet.FileSystems; using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Generator.Compiler.CSharp; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Text; +using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.Razor @@ -106,7 +108,7 @@ Environment.NewLine + public void ModelVisitor_GeneratesCorrectLineMappings() { // Arrange - var host = new MvcRazorHost("RazorView") + var host = new MvcRazorHost("appRoot", Mock.Of()) { DesignTimeMode = true }; @@ -116,7 +118,7 @@ Environment.NewLine + var expectedCode = ReadResource("TestFiles/Output/Model.cs"); var expectedLineMappings = new List { - BuildLineMapping(7, 0, 7, 126, 6, 7, 30), + BuildLineMapping(7, 0, 7, 151, 6, 7, 30), }; // Act @@ -146,7 +148,7 @@ Environment.NewLine + private static CodeGeneratorContext CreateContext() { - return CodeGeneratorContext.Create(new MvcRazorHost("RazorView"), + return CodeGeneratorContext.Create(new MvcRazorHost("appRoot", Mock.Of()), "MyClass", "MyNamespace", string.Empty, diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileSystem.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileSystem.cs new file mode 100644 index 000000000..68b2a5864 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileSystem.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.AspNet.FileSystems; +using Moq; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class TestFileSystem : IFileSystem + { + private readonly Dictionary _lookup = + new Dictionary(StringComparer.Ordinal); + + public bool TryGetDirectoryContents(string subpath, out IEnumerable contents) + { + throw new NotImplementedException(); + } + + public void AddFile(string path, string contents) + { + var fileInfo = new Mock(); + fileInfo.Setup(f => f.CreateReadStream()) + .Returns(new MemoryStream(Encoding.UTF8.GetBytes(contents))); + fileInfo.SetupGet(f => f.PhysicalPath) + .Returns(path); + fileInfo.SetupGet(f => f.Name) + .Returns(Path.GetFileName(path)); + AddFile(path, fileInfo.Object); + } + + public void AddFile(string path, IFileInfo contents) + { + _lookup.Add(path, contents); + } + + public bool TryGetFileInfo(string subpath, out IFileInfo fileInfo) + { + return _lookup.TryGetValue(subpath, out fileInfo); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs index af51698d3..f8a947a10 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs @@ -1,4 +1,4 @@ -namespace Razor +namespace Asp { #line 1 "" using MyNamespace @@ -8,7 +8,7 @@ using MyNamespace ; using System.Threading.Tasks; - public class __CompiledTemplate : RazorView + public class __CompiledTemplate : Microsoft.AspNet.Mvc.Razor.RazorPage { private static object @__o; private void @__RazorDesignTimeHelpers__() diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/InjectWithModel.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/InjectWithModel.cs index 81df5c49e..cb3fd992d 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/InjectWithModel.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/InjectWithModel.cs @@ -1,8 +1,8 @@ -namespace Razor +namespace Asp { using System.Threading.Tasks; - public class __CompiledTemplate : RazorView< + public class __CompiledTemplate : Microsoft.AspNet.Mvc.Razor.RazorPage< #line 1 "" MyModel @@ -32,7 +32,7 @@ [Microsoft.AspNet.Mvc.ActivateAttribute] public #line 3 "" - MyService Html + MyService Html #line default #line hidden diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Model.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Model.cs index 523b36e68..2cc67b2a6 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Model.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Model.cs @@ -1,8 +1,8 @@ -namespace Razor +namespace Asp { using System.Threading.Tasks; - public class __CompiledTemplate : RazorView< + public class __CompiledTemplate : Microsoft.AspNet.Mvc.Razor.RazorPage< #line 1 "" System.Collections.IEnumerable diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/ViewStartProviderTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/ViewStartUtilityTest.cs similarity index 60% rename from test/Microsoft.AspNet.Mvc.Razor.Test/ViewStartProviderTest.cs rename to test/Microsoft.AspNet.Mvc.Razor.Host.Test/ViewStartUtilityTest.cs index 8ccfbdfec..9ce7fe3f3 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/ViewStartProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/ViewStartUtilityTest.cs @@ -2,12 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.Diagnostics; -using Microsoft.Framework.Runtime; -using Moq; using Xunit; -namespace Microsoft.AspNet.Mvc.Razor.Test +namespace Microsoft.AspNet.Mvc.Razor { public class ViewStartProviderTest { @@ -18,10 +15,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test { // Arrange var appPath = @"x:\test"; - var provider = new ViewStartProvider(GetAppEnv(appPath), Mock.Of()); // Act - var result = provider.GetViewStartLocations(viewPath); + var result = ViewStartUtility.GetViewStartLocations(appPath, viewPath); // Assert Assert.Empty(result); @@ -37,9 +33,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test "/Views/Home/View.cshtml", new[] { - @"x:\test\myapp\Views\Home\_ViewStart.cshtml", - @"x:\test\myapp\Views\_ViewStart.cshtml", - @"x:\test\myapp\_ViewStart.cshtml", + @"x:\test\myapp\Views\Home\_viewstart.cshtml", + @"x:\test\myapp\Views\_viewstart.cshtml", + @"x:\test\myapp\_viewstart.cshtml", } }; @@ -49,9 +45,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test "Views/Home/View.cshtml", new[] { - @"x:\test\myapp\Views\Home\_ViewStart.cshtml", - @"x:\test\myapp\Views\_ViewStart.cshtml", - @"x:\test\myapp\_ViewStart.cshtml", + @"x:\test\myapp\Views\Home\_viewstart.cshtml", + @"x:\test\myapp\Views\_viewstart.cshtml", + @"x:\test\myapp\_viewstart.cshtml", } }; @@ -61,9 +57,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test "Views/Home/View.cshtml", new[] { - @"x:\test\myapp\Views\Home\_ViewStart.cshtml", - @"x:\test\myapp\Views\_ViewStart.cshtml", - @"x:\test\myapp\_ViewStart.cshtml", + @"x:\test\myapp\Views\Home\_viewstart.cshtml", + @"x:\test\myapp\Views\_viewstart.cshtml", + @"x:\test\myapp\_viewstart.cshtml", } }; } @@ -75,22 +71,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Test string viewPath, IEnumerable expected) { - // Arrange - var provider = new ViewStartProvider(GetAppEnv(appPath), Mock.Of()); - // Act - var result = provider.GetViewStartLocations(viewPath); + var result = ViewStartUtility.GetViewStartLocations(appPath, viewPath); // Assert Assert.Equal(expected, result); } - - private static IApplicationEnvironment GetAppEnv(string appPath) - { - var appEnv = new Mock(); - appEnv.Setup(p => p.ApplicationBasePath) - .Returns(appPath); - return appEnv.Object; - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json index 928940248..95907e438 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json @@ -4,7 +4,8 @@ }, "resources": "TestFiles\\**", "dependencies": { - "Microsoft.AspNet.Mvc.Razor.Host" : "", + "Microsoft.AspNet.Mvc.Razor.Host": "", + "Microsoft.AspNet.Testing": "1.0.0-*", "Xunit.KRunner": "1.0.0-*" }, "commands": { diff --git a/test/WebSites/RazorWebSite/Controllers/DirectivesController.cs b/test/WebSites/RazorWebSite/Controllers/DirectivesController.cs new file mode 100644 index 000000000..33d85cd3e --- /dev/null +++ b/test/WebSites/RazorWebSite/Controllers/DirectivesController.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using Microsoft.AspNet.Mvc; + +namespace RazorWebSite +{ + public class DirectivesController : Controller + { + public ViewResult ViewInheritsInjectAndUsingsFromViewStarts() + { + return View(new Person { Name = "Person1" }); + } + + public ViewResult ViewInheritsBasePageFromViewStarts() + { + return View("/views/directives/scoped/ViewInheritsBasePageFromViewStarts.cshtml", + new Person { Name = "Person2" }); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/MyBasePage.cs b/test/WebSites/RazorWebSite/MyBasePage.cs new file mode 100644 index 000000000..07039b6ca --- /dev/null +++ b/test/WebSites/RazorWebSite/MyBasePage.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using Microsoft.AspNet.Mvc.Razor; + +namespace RazorWebSite +{ + public abstract class MyBasePage : RazorPage + { + public override void WriteLiteral(object value) + { + base.WriteLiteral("WriteLiteral says:"); + base.WriteLiteral(value); + } + + public override void Write(object value) + { + base.WriteLiteral("Write says:"); + base.Write(value); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Services/InjectedHelper.cs b/test/WebSites/RazorWebSite/Services/InjectedHelper.cs new file mode 100644 index 000000000..fe98263aa --- /dev/null +++ b/test/WebSites/RazorWebSite/Services/InjectedHelper.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace RazorWebSite +{ + public class InjectedHelper + { + public string Greet(Person person) + { + return "Hello " + person.Name; + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Startup.cs b/test/WebSites/RazorWebSite/Startup.cs index 015d25f56..951a918a2 100644 --- a/test/WebSites/RazorWebSite/Startup.cs +++ b/test/WebSites/RazorWebSite/Startup.cs @@ -14,6 +14,7 @@ namespace RazorWebSite { // Add MVC services to the services container services.AddMvc(configuration); + services.AddTransient(); }); // Add MVC to the request pipeline diff --git a/test/WebSites/RazorWebSite/Views/Directives/Scoped/ViewInheritsBasePageFromViewStarts.cshtml b/test/WebSites/RazorWebSite/Views/Directives/Scoped/ViewInheritsBasePageFromViewStarts.cshtml new file mode 100644 index 000000000..cdb1b35b3 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Directives/Scoped/ViewInheritsBasePageFromViewStarts.cshtml @@ -0,0 +1 @@ +@MyHelper.Greet(Model) \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/Directives/Scoped/_Layout.cshtml b/test/WebSites/RazorWebSite/Views/Directives/Scoped/_Layout.cshtml new file mode 100644 index 000000000..7e6b5f655 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Directives/Scoped/_Layout.cshtml @@ -0,0 +1 @@ +layout:@RenderBody() \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/Directives/Scoped/_ViewStart.cshtml b/test/WebSites/RazorWebSite/Views/Directives/Scoped/_ViewStart.cshtml new file mode 100644 index 000000000..4867deaa3 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Directives/Scoped/_ViewStart.cshtml @@ -0,0 +1,4 @@ +@inherits MyBasePage +@{ + Layout = "/Views/Directives/Scoped/_Layout.cshtml"; +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/Directives/ViewInheritsInjectAndUsingsFromViewStarts.cshtml b/test/WebSites/RazorWebSite/Views/Directives/ViewInheritsInjectAndUsingsFromViewStarts.cshtml new file mode 100644 index 000000000..e1a2a0671 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Directives/ViewInheritsInjectAndUsingsFromViewStarts.cshtml @@ -0,0 +1,2 @@ +@model MyPerson +@MyHelper.Greet(Model) \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/Directives/_ViewStart.cshtml b/test/WebSites/RazorWebSite/Views/Directives/_ViewStart.cshtml new file mode 100644 index 000000000..68a051a32 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Directives/_ViewStart.cshtml @@ -0,0 +1,2 @@ +@using MyPerson = RazorWebSite.Person +@inject InjectedHelper MyHelper diff --git a/test/WebSites/RazorWebSite/Views/Shared/_Partial.cshtml b/test/WebSites/RazorWebSite/Views/Shared/_Partial.cshtml index 515b4a999..755e2958b 100644 --- a/test/WebSites/RazorWebSite/Views/Shared/_Partial.cshtml +++ b/test/WebSites/RazorWebSite/Views/Shared/_Partial.cshtml @@ -1,2 +1,2 @@ -@model RazorWebSite.Address +@model Address @ViewData.Model.ZipCode diff --git a/test/WebSites/RazorWebSite/Views/_ViewStart.cshtml b/test/WebSites/RazorWebSite/Views/_ViewStart.cshtml new file mode 100644 index 000000000..44ab54173 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/_ViewStart.cshtml @@ -0,0 +1 @@ +@using RazorWebSite \ No newline at end of file