diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ApiSets/GeneratedTagHelperAttributeContext.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ApiSets/GeneratedTagHelperAttributeContext.cs deleted file mode 100644 index f21f1511ed..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ApiSets/GeneratedTagHelperAttributeContext.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Mvc.Razor -{ - /// - /// Contains information for the tag helper attribute code - /// generation process. - /// - public class GeneratedTagHelperAttributeContext - { - /// - /// Name of the model expression type. - /// - public string ModelExpressionTypeName { get; set; } - - /// - /// Name the method to create ModelExpressions. - /// - public string CreateModelExpressionMethodName { get; set; } - - /// - /// Gets or sets the name of the IModelExpressionProvider. - /// - public string ModelExpressionProviderPropertyName { get; set; } - - /// - /// Gets or sets the property name of the ViewDataDictionary. - /// - public string ViewDataPropertyName { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ApiSets/GeneratedViewComponentTagHelperContext.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ApiSets/GeneratedViewComponentTagHelperContext.cs deleted file mode 100644 index 06ef983176..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ApiSets/GeneratedViewComponentTagHelperContext.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Mvc.Razor.Host.Internal -{ - /// - /// Contains necessary information for the view component tag helper code generation process. - /// - public class GeneratedViewComponentTagHelperContext - { - /// - /// Instantiates a new instance of the with default values. - /// - public GeneratedViewComponentTagHelperContext() - { - ContextualizeMethodName = "Contextualize"; - InvokeAsyncMethodName = "InvokeAsync"; - IViewComponentHelperTypeName = "Microsoft.AspNetCore.Mvc.IViewComponentHelper"; - IViewContextAwareTypeName = "Microsoft.AspNetCore.Mvc.ViewFeatures.IViewContextAware"; - ViewContextAttributeTypeName = "Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute"; - ViewContextTypeName = "Microsoft.AspNetCore.Mvc.Rendering.ViewContext"; - } - - /// - /// Name of the Contextualize method called by an instance of the IViewContextAware type. - /// - public string ContextualizeMethodName { get; set; } - - /// - /// Name of the InvokeAsync method called by an IViewComponentHelper. - /// - public string InvokeAsyncMethodName { get; set; } - - /// - /// Name of the IViewComponentHelper type used to invoke view components. - /// - public string IViewComponentHelperTypeName { get; set; } - - /// - /// Name of the IViewContextAware type used to contextualize the view context. - /// - public string IViewContextAwareTypeName { get; set; } - - /// - /// Name of the ViewContext type for view execution. - /// - public string ViewContextTypeName { get; set; } - - /// - /// Name of the ViewContextAttribute type. - /// - public string ViewContextAttributeTypeName { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/InjectDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/InjectDirective.cs index 6af43444c1..e37a029594 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/InjectDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/InjectDirective.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host return builder; } - private class Pass : IRazorIRPass + internal class Pass : IRazorIRPass { public RazorEngine Engine { get; set; } @@ -31,18 +31,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host var visitor = new Visitor(); visitor.Visit(irDocument); - for (var i = 0; i < visitor.Directives.Count; i++) + var properties = new HashSet(StringComparer.Ordinal); + + for (var i = visitor.Directives.Count - 1; i >= 0; i--) { var directive = visitor.Directives[i]; - var typeName = directive.Tokens.ElementAt(0).Content;; - var memberName = directive.Tokens.ElementAt(1).Content; - - var modelType = "dynamic"; - if (visitor.ModelType.Count > 0) + var tokens = directive.Tokens.ToArray(); + if (tokens.Length < 2) { - modelType = visitor.ModelType.Last().Tokens.First().Content; + continue; } + var typeName = tokens[0].Content; + var memberName = tokens[1].Content; + + if (!properties.Add(memberName)) + { + continue; + } + + var modelType = ModelDirective.GetModelType(irDocument); + typeName = typeName.Replace("", "<" + modelType + ">"); var member = new CSharpStatementIRNode() diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs index 7c9eced429..ad54ed4afb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. 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.AspNetCore.Razor.Evolution; @@ -19,6 +20,35 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host return builder; } + public static string GetModelType(DocumentIRNode document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + var visitor = new Visitor(); + visitor.Visit(document); + + return GetModelType(visitor); + } + + private static string GetModelType(Visitor visitor) + { + for (var i = visitor.ModelDirectives.Count - 1; i >= 0; i--) + { + var directive = visitor.ModelDirectives[i]; + + var tokens = directive.Tokens.ToArray(); + if (tokens.Length >= 1) + { + return tokens[0].Content; + } + } + + return "dynamic"; + } + private class Pass : IRazorIRPass { public RazorEngine Engine { get; set; } @@ -31,13 +61,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host var visitor = new Visitor(); visitor.Visit(irDocument); - string modelType = "dynamic"; - if (visitor.Directives.Count == 1) + var modelType = GetModelType(visitor); + + var baseType = visitor.Class.BaseType; + for (var i = visitor.InheritsDirectives.Count - 1; i >= 0; i--) { - modelType = visitor.Directives.Last().Tokens.First().Content; + var directive = visitor.InheritsDirectives[i]; + var tokens = directive.Tokens.ToArray(); + if (tokens.Length >= 1) + { + baseType = tokens[0].Content; + break; + } } - visitor.Class.BaseType = visitor.Class.BaseType.Replace("", "<" + modelType + ">"); + visitor.Class.BaseType = baseType.Replace("", "<" + modelType + ">"); return irDocument; } @@ -47,7 +85,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host { public ClassDeclarationIRNode Class { get; private set; } - public IList Directives { get; } = new List(); + public IList InheritsDirectives { get; } = new List(); + + public IList ModelDirectives { get; } = new List(); public override void VisitClass(ClassDeclarationIRNode node) { @@ -63,7 +103,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host { if (node.Descriptor == Directive) { - Directives.Add(node); + ModelDirectives.Add(node); + } + else if (node.Descriptor.Name == "inherits") + { + InheritsDirectives.Add(node); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelExpressionPass.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelExpressionPass.cs index 54c77baa27..ef8c93b678 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelExpressionPass.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelExpressionPass.cs @@ -38,9 +38,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host Content = "ModelExpressionProvider.CreateModelExpression(ViewData, __model => ", }); - if (node.Children.Count == 1 && node.Children[0] is HtmlContentIRNode) { + // A 'simple' expression will look like __model => __model.Foo + // + // Note that the fact we're looking for HTML here is based on a bug. + // https://github.com/aspnet/Razor/issues/963 var original = ((HtmlContentIRNode)node.Children[0]); builder.Add(new CSharpTokenIRNode() @@ -58,7 +61,32 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host { for (var i = 0; i < node.Children.Count; i++) { - builder.Add(node.Children[i]); + var nestedExpression = node.Children[i] as CSharpExpressionIRNode; + if (nestedExpression != null) + { + for (var j = 0; j < nestedExpression.Children.Count; j++) + { + var cSharpToken = nestedExpression.Children[j] as CSharpTokenIRNode; + if (cSharpToken != null) + { + builder.Add(cSharpToken); + } + } + + continue; + } + + // Note that the fact we're looking for HTML here is based on a bug. + // https://github.com/aspnet/Razor/issues/963 + var html = node.Children[i] as HtmlContentIRNode; + if (html != null) + { + builder.Add(new CSharpTokenIRNode() + { + Content = html.Content, + Source = html.Source, + }); + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/exceptions.net45.json b/src/Microsoft.AspNetCore.Mvc.Razor.Host/exceptions.net45.json index ae2b509a14..3ca07cfc87 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/exceptions.net45.json +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/exceptions.net45.json @@ -90,5 +90,9 @@ { "OldTypeId": "public class Microsoft.AspNetCore.Mvc.Razor.Directives.UsingChunkMerger : Microsoft.AspNetCore.Mvc.Razor.Directives.IChunkMerger", "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Mvc.Razor.GeneratedTagHelperAttributeContext", + "Kind": "Removal" } ] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/exceptions.netcore.json b/src/Microsoft.AspNetCore.Mvc.Razor.Host/exceptions.netcore.json index ae2b509a14..3ca07cfc87 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/exceptions.netcore.json +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/exceptions.netcore.json @@ -90,5 +90,9 @@ { "OldTypeId": "public class Microsoft.AspNetCore.Mvc.Razor.Directives.UsingChunkMerger : Microsoft.AspNetCore.Mvc.Razor.Directives.IChunkMerger", "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Mvc.Razor.GeneratedTagHelperAttributeContext", + "Kind": "Removal" } ] \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Directives/InjectChunkMergerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Directives/InjectChunkMergerTest.cs deleted file mode 100644 index 41022d1c0c..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Directives/InjectChunkMergerTest.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Razor.Chunks; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Razor.Directives -{ - public class InjectChunkMergerTest - { -#if OLD_RAZOR - [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_AddsChunkIfChunkWithMatchingPropertyNameWasNotVisitedInChunkTree() - { - // Arrange - var expectedType = "MyApp.MyHelperType"; - var expectedProperty = "MyHelper"; - var merger = new InjectChunkMerger("dynamic"); - var chunkTree = new ChunkTree(); - var inheritedChunks = new[] - { - new InjectChunk(expectedType, expectedProperty) - }; - - // Act - merger.MergeInheritedChunks(chunkTree, inheritedChunks); - - // Assert - var chunk = Assert.Single(chunkTree.Children); - var injectChunk = Assert.IsType(chunk); - Assert.Equal(expectedType, injectChunk.TypeName); - Assert.Equal(expectedProperty, injectChunk.MemberName); - } - - [Fact] - public void Merge_IgnoresChunkIfChunkWithMatchingPropertyNameWasVisitedInChunkTree() - { - // Arrange - var merger = new InjectChunkMerger("dynamic"); - var chunkTree = new ChunkTree(); - var inheritedChunks = new[] - { - new InjectChunk("MyTypeB", "MyProperty") - }; - - // Act - merger.VisitChunk(new InjectChunk("MyTypeA", "MyProperty")); - merger.MergeInheritedChunks(chunkTree, inheritedChunks); - - // Assert - Assert.Empty(chunkTree.Children); - } - - [Fact] - public void Merge_MatchesPropertyNameInCaseSensitiveManner() - { - // Arrange - var merger = new InjectChunkMerger("dynamic"); - var chunkTree = new ChunkTree(); - var inheritedChunks = new[] - { - new InjectChunk("MyTypeB", "different-property"), - new InjectChunk("MyType", "myproperty"), - }; - - // Act - merger.VisitChunk(new InjectChunk("MyType", "MyProperty")); - merger.MergeInheritedChunks(chunkTree, inheritedChunks); - - // Assert - Assert.Equal(2, chunkTree.Children.Count); - var injectChunk = Assert.IsType(chunkTree.Children[0]); - Assert.Equal("MyType", injectChunk.TypeName); - Assert.Equal("myproperty", injectChunk.MemberName); - - injectChunk = Assert.IsType(chunkTree.Children[1]); - Assert.Equal("MyTypeB", injectChunk.TypeName); - Assert.Equal("different-property", injectChunk.MemberName); - } - - [Fact] - public void Merge_ResolvesModelNameInTypesWithTModelToken() - { - // Arrange - var merger = new InjectChunkMerger("dynamic"); - var chunkTree = new ChunkTree(); - var inheritedChunks = new[] - { - new InjectChunk("MyHelper", "MyProperty") - }; - - // Act - merger.MergeInheritedChunks(chunkTree, inheritedChunks); - - // Assert - var chunk = Assert.Single(chunkTree.Children); - 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 chunkTree = new ChunkTree(); - var inheritedChunks = new[] - { - new InjectChunk("MyHelper", "MyProperty") - }; - - // Act - merger.MergeInheritedChunks(chunkTree, inheritedChunks); - - // Assert - var chunk = Assert.Single(chunkTree.Children); - var injectChunk = Assert.IsType(chunk); - Assert.Equal("MyHelper", injectChunk.TypeName); - Assert.Equal("MyProperty", injectChunk.MemberName); - } - - [Fact] - public void Merge_UsesTheLastInjectChunkOfAPropertyName() - { - // Arrange - var merger = new InjectChunkMerger("dynamic"); - var chunkTree = new ChunkTree(); - var inheritedChunks = new Chunk[] - { - new LiteralChunk(), - new InjectChunk("SomeOtherType", "Property"), - new InjectChunk("DifferentPropertyType", "DifferentProperty"), - new InjectChunk("SomeType", "Property"), - }; - - // Act - merger.MergeInheritedChunks(chunkTree, inheritedChunks); - - // Assert - Assert.Collection(chunkTree.Children, - chunk => - { - var injectChunk = Assert.IsType(chunk); - Assert.Equal("SomeType", injectChunk.TypeName); - Assert.Equal("Property", injectChunk.MemberName); - }, - chunk => - { - var injectChunk = Assert.IsType(chunk); - Assert.Equal("DifferentPropertyType", injectChunk.TypeName); - Assert.Equal("DifferentProperty", injectChunk.MemberName); - }); - } -#endif - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs deleted file mode 100644 index 1063f4026f..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Mvc.Razor.Directives; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Chunks; -using Microsoft.AspNetCore.Razor.Chunks.Generators; -using Microsoft.AspNetCore.Razor.CodeGenerators; -using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; -using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Razor -{ - public class InjectChunkVisitorTest - { - #if OLD_RAZOR - [Fact] - public void Visit_IgnoresNonInjectChunks() - { - // Arrange - var writer = new CSharpCodeWriter(); - var context = CreateContext(); - - var visitor = new InjectChunkVisitor(writer, context, "ActivateAttribute"); - - // Act - visitor.Accept(new Chunk[] - { - new LiteralChunk(), - new CodeAttributeChunk() - }); - var code = writer.GenerateCode(); - - // Assert - Assert.Empty(code); - } - - [Fact] - public void Visit_GeneratesProperties_ForInjectChunks() - { - // Arrange - var expected = -@"[ActivateAttribute] -public MyType1 MyPropertyName1 { get; private set; } -[ActivateAttribute] -public MyType2 @MyPropertyName2 { get; private set; } -"; - var writer = new CSharpCodeWriter(); - var context = CreateContext(); - - var visitor = new InjectChunkVisitor(writer, context, "ActivateAttribute"); - var factory = SpanFactory.CreateCsHtml(); - var node = (Span)factory.Code("Some code") - .As(new InjectParameterGenerator("MyType", "MyPropertyName")); - - // Act - visitor.Accept(new Chunk[] - { - new LiteralChunk(), - new InjectChunk("MyType1", "MyPropertyName1") { Association = node }, - new InjectChunk("MyType2", "@MyPropertyName2") { Association = node } - }); - var code = writer.GenerateCode(); - - // Assert - Assert.Equal(expected, code); - } - - [Fact] - public void Visit_WithDesignTimeHost_GeneratesPropertiesAndLinePragmas_ForInjectChunks() - { - // Arrange - var expected = string.Join(Environment.NewLine, -"[Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]", -"public", -@"#line 1 """"", -"MyType1 MyPropertyName1", -"", -"#line default", -"#line hidden", -"{ get; private set; }", -"[Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]", -"public", -@"#line 1 """"", -"MyType2 @MyPropertyName2", -"", -"#line default", -"#line hidden", -"{ get; private set; }", -""); - var writer = new CSharpCodeWriter(); - var context = CreateContext(); - context.Host.DesignTimeMode = true; - - var visitor = new InjectChunkVisitor(writer, context, "Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute"); - var factory = SpanFactory.CreateCsHtml(); - var node = (Span)factory.Code("Some code") - .As(new InjectParameterGenerator("MyType", "MyPropertyName")); - - // Act - visitor.Accept(new Chunk[] - { - new LiteralChunk(), - new InjectChunk("MyType1", "MyPropertyName1") { Association = node }, - new InjectChunk("MyType2", "@MyPropertyName2") { Association = node } - }); - var code = writer.GenerateCode(); - - // Assert - Assert.Equal(expected, code); - } - - [Fact] - public void Visit_WithDesignTimeHost_GeneratesPropertiesAndLinePragmas_ForPartialInjectChunks() - { - // Arrange - var expected = @"[Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] -public -#line 1 """" -MyType1 - -#line default -#line hidden -{ get; private set; } -"; - var writer = new CSharpCodeWriter(); - var context = CreateContext(); - context.Host.DesignTimeMode = true; - - var visitor = new InjectChunkVisitor(writer, context, "Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute"); - var factory = SpanFactory.CreateCsHtml(); - var node = (Span)factory.Code("Some code") - .As(new InjectParameterGenerator("MyType", "MyPropertyName")); - - // Act - visitor.Accept(new Chunk[] - { - new LiteralChunk(), - new InjectChunk("MyType1", string.Empty) { Association = node }, - }); - var code = writer.GenerateCode(); - - // Assert - Assert.Equal(expected, code); - } - - private static CodeGeneratorContext CreateContext() - { - var chunkTreeCache = new DefaultChunkTreeCache(new TestFileProvider()); - return new CodeGeneratorContext( - new ChunkGeneratorContext( - new MvcRazorHost(chunkTreeCache, new TagHelperDescriptorResolver(designTime: false)), - "MyClass", - "MyNamespace", - string.Empty, - shouldGenerateLinePragmas: true), - new ErrorSink()); - } -#endif - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/InjectDirectiveTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/InjectDirectiveTest.cs new file mode 100644 index 0000000000..89fa0b9492 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/InjectDirectiveTest.cs @@ -0,0 +1,241 @@ +// Copyright (c) .NET Foundation. 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.IO; +using System.Text; +using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Host +{ + public class InjectDirectiveTest + { + [Fact] + public void InjectDirectivePass_Execute_DefinesProperty() + { + // Arrange + var codeDocument = CreateDocument(@" +@inject PropertyType PropertyName +"); + + var engine = CreateEngine(); + var pass = new InjectDirective.Pass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var @class = FindClassNode(irDocument); + Assert.NotNull(@class); + Assert.Equal(2, @class.Children.Count); + + var statement = Assert.IsType(@class.Children[1]); + Assert.Equal( + "[Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]" + Environment.NewLine + + "public PropertyType PropertyName { get; private set; }", + statement.Content); + } + + [Fact] + public void InjectDirectivePass_Execute_DedupesPropertiesByName() + { + // Arrange + var codeDocument = CreateDocument(@" +@inject PropertyType PropertyName +@inject PropertyType2 PropertyName +"); + + var engine = CreateEngine(); + var pass = new InjectDirective.Pass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var @class = FindClassNode(irDocument); + Assert.NotNull(@class); + Assert.Equal(2, @class.Children.Count); + + var statement = Assert.IsType(@class.Children[1]); + Assert.Equal( + "[Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]" + Environment.NewLine + + "public PropertyType2 PropertyName { get; private set; }", + statement.Content); + } + + [Fact] + public void InjectDirectivePass_Execute_ExpandsTModel_WithDynamic() + { + // Arrange + var codeDocument = CreateDocument(@" +@inject PropertyType PropertyName +"); + + var engine = CreateEngine(); + var pass = new InjectDirective.Pass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var @class = FindClassNode(irDocument); + Assert.NotNull(@class); + Assert.Equal(2, @class.Children.Count); + + var statement = Assert.IsType(@class.Children[1]); + Assert.Equal( + "[Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]" + Environment.NewLine + + "public PropertyType PropertyName { get; private set; }", + statement.Content); + } + + [Fact] + public void InjectDirectivePass_Execute_ExpandsTModel_WithModelTypeFirst() + { + // Arrange + var codeDocument = CreateDocument(@" +@model ModelType +@inject PropertyType PropertyName +"); + + var engine = CreateEngine(); + var pass = new InjectDirective.Pass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var @class = FindClassNode(irDocument); + Assert.NotNull(@class); + Assert.Equal(2, @class.Children.Count); + + var statement = Assert.IsType(@class.Children[1]); + Assert.Equal( + "[Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]" + Environment.NewLine + + "public PropertyType PropertyName { get; private set; }", + statement.Content); + } + + [Fact] + public void InjectDirectivePass_Execute_ExpandsTModel_WithModelType() + { + // Arrange + var codeDocument = CreateDocument(@" +@inject PropertyType PropertyName +@model ModelType +"); + + var engine = CreateEngine(); + var pass = new InjectDirective.Pass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var @class = FindClassNode(irDocument); + Assert.NotNull(@class); + Assert.Equal(2, @class.Children.Count); + + var statement = Assert.IsType(@class.Children[1]); + Assert.Equal( + "[Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]" + Environment.NewLine + + "public PropertyType PropertyName { get; private set; }", + statement.Content); + } + + private RazorCodeDocument CreateDocument(string content) + { + using (var stream = new MemoryStream()) + { + var bytes = Encoding.UTF8.GetBytes(content); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0L, SeekOrigin.Begin); + + var source = RazorSourceDocument.ReadFrom(stream, "test.cshtml"); + return RazorCodeDocument.Create(source); + } + } + + private ClassDeclarationIRNode FindClassNode(RazorIRNode node) + { + var visitor = new ClassNodeVisitor(); + visitor.Visit(node); + return visitor.Node; + } + + private RazorEngine CreateEngine() + { + return RazorEngine.Create(b => + { + // Notice we're not registering the InjectDirective.Pass here so we can run it on demand. + b.AddDirective(InjectDirective.Directive); + b.AddDirective(ModelDirective.Directive); + }); + } + + private DocumentIRNode CreateIRDocument(RazorEngine engine, RazorCodeDocument codeDocument) + { + for (var i = 0; i < engine.Phases.Count; i++) + { + var phase = engine.Phases[i]; + phase.Execute(codeDocument); + + if (phase is IRazorIRPhase) + { + break; + } + } + + return codeDocument.GetIRDocument(); + } + + private string GetCSharpContent(RazorIRNode node) + { + var builder = new StringBuilder(); + for (var i = 0; i < node.Children.Count; i++) + { + var child = node.Children[i] as CSharpTokenIRNode; + builder.Append(child.Content); + } + + return builder.ToString(); + } + + private class ClassNodeVisitor : RazorIRNodeWalker + { + public ClassDeclarationIRNode Node { get; set; } + + public override void VisitClass(ClassDeclarationIRNode node) + { + Node = node; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/TagHelperChunkDecoratorTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/TagHelperChunkDecoratorTest.cs index 03bbea9b86..d6edfdd2d9 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/TagHelperChunkDecoratorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/TagHelperChunkDecoratorTest.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using Microsoft.AspNetCore.Mvc.Razor.Host.Internal; using Microsoft.AspNetCore.Razor.Chunks; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Moq; diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/ViewComponentTagHelperChunkVisitorTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/ViewComponentTagHelperChunkVisitorTest.cs index bf3257abd5..f926e1e8b9 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/ViewComponentTagHelperChunkVisitorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/ViewComponentTagHelperChunkVisitorTest.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Reflection; -using Microsoft.AspNetCore.Mvc.Razor.Host.Internal; using Microsoft.AspNetCore.Razor.Chunks; using Microsoft.AspNetCore.Razor.CodeGenerators; using Xunit; diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ModelExpressionPassTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ModelExpressionPassTest.cs new file mode 100644 index 0000000000..8662152a64 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ModelExpressionPassTest.cs @@ -0,0 +1,258 @@ +// Copyright (c) .NET Foundation. 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; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Xunit; +using ErrorSink = Microsoft.AspNetCore.Razor.Evolution.Legacy.ErrorSink; + +namespace Microsoft.AspNetCore.Mvc.Razor.Host +{ + public class MvcTagHelperAttributeValueCodeRendererTest + { + [Fact] + public void ModelExpressionPass_NonModelExpressionProperty_Ignored() + { + // Arrange + var codeDocument = CreateDocument(@" +@addTagHelper TestTagHelper, TestAssembly +

"); + + var tagHelpers = new[] + { + new TagHelperDescriptor() + { + AssemblyName = "TestAssembly", + TypeName = "TestTagHelper", + TagName = "p", + Attributes = new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor() + { + TypeName = "System.Int32", + Name = "Foo", + } + + } + } + }; + + var engine = CreateEngine(tagHelpers); + var pass = new ModelExpressionPass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var tagHelper = FindTagHelperNode(irDocument); + var setProperty = tagHelper.Children.OfType().Single(); + + var child = Assert.IsType(Assert.Single(setProperty.Children)); + Assert.Equal("17", child.Content); + } + + [Fact] + public void ModelExpressionPass_ModelExpressionProperty_SimpleExpression() + { + // Arrange + var codeDocument = CreateDocument(@" +@addTagHelper TestTagHelper, TestAssembly +

"); + + var tagHelpers = new[] + { + new TagHelperDescriptor() + { + AssemblyName = "TestAssembly", + TypeName = "TestTagHelper", + TagName = "p", + Attributes = new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor() + { + TypeName = typeof(ModelExpression).FullName, + Name = "Foo", + } + + } + } + }; + + var engine = CreateEngine(tagHelpers); + var pass = new ModelExpressionPass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var tagHelper = FindTagHelperNode(irDocument); + var setProperty = tagHelper.Children.OfType().Single(); + + var expression = Assert.IsType(Assert.Single(setProperty.Children)); + Assert.Equal("ModelExpressionProvider.CreateModelExpression(ViewData, __model => __model.Bar)", GetCSharpContent(expression)); + + var originalNode = Assert.IsType(expression.Children[2]); + Assert.Equal("Bar", originalNode.Content); + Assert.Equal(new SourceSpan("test.cshtml", 53, 2, 8, 3), originalNode.Source.Value); + } + + [Fact] + public void ModelExpressionPass_ModelExpressionProperty_ComplexExpression() + { + // Arrange + var codeDocument = CreateDocument(@" +@addTagHelper TestTagHelper, TestAssembly +

"); + + var tagHelpers = new[] + { + new TagHelperDescriptor() + { + AssemblyName = "TestAssembly", + TypeName = "TestTagHelper", + TagName = "p", + Attributes = new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor() + { + TypeName = typeof(ModelExpression).FullName, + Name = "Foo", + } + + } + } + }; + + var engine = CreateEngine(tagHelpers); + var pass = new ModelExpressionPass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var tagHelper = FindTagHelperNode(irDocument); + var setProperty = tagHelper.Children.OfType().Single(); + + var expression = Assert.IsType(Assert.Single(setProperty.Children)); + Assert.Equal("ModelExpressionProvider.CreateModelExpression(ViewData, __model => Bar)", GetCSharpContent(expression)); + + var originalNode = Assert.IsType(expression.Children[1]); + Assert.Equal("Bar", originalNode.Content); + Assert.Equal(new SourceSpan("test.cshtml", 54, 2, 9, 3), originalNode.Source.Value); + } + + private RazorCodeDocument CreateDocument(string content) + { + using (var stream = new MemoryStream()) + { + var bytes = Encoding.UTF8.GetBytes(content); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0L, SeekOrigin.Begin); + + var source = RazorSourceDocument.ReadFrom(stream, "test.cshtml"); + return RazorCodeDocument.Create(source); + } + } + + private RazorEngine CreateEngine(params TagHelperDescriptor[] tagHelpers) + { + return RazorEngine.Create(b => + { + b.Features.Add(new TagHelperFeature(tagHelpers)); + }); + } + + private DocumentIRNode CreateIRDocument(RazorEngine engine, RazorCodeDocument codeDocument) + { + for (var i = 0; i < engine.Phases.Count; i++) + { + var phase = engine.Phases[i]; + phase.Execute(codeDocument); + + if (phase is IRazorIRPhase) + { + break; + } + } + + return codeDocument.GetIRDocument(); + } + + private TagHelperIRNode FindTagHelperNode(RazorIRNode node) + { + var visitor = new TagHelperNodeVisitor(); + visitor.Visit(node); + return visitor.Node; + } + + private string GetCSharpContent(RazorIRNode node) + { + var builder = new StringBuilder(); + for (var i = 0; i < node.Children.Count; i++) + { + var child = node.Children[i] as CSharpTokenIRNode; + builder.Append(child.Content); + } + + return builder.ToString(); + } + + private class TagHelperNodeVisitor : RazorIRNodeWalker + { + public TagHelperIRNode Node { get; set; } + + public override void VisitTagHelper(TagHelperIRNode node) + { + Node = node; + } + } + + private class TagHelperFeature : ITagHelperFeature + { + public TagHelperFeature(TagHelperDescriptor[] tagHelpers) + { + Resolver = new TagHelperDescriptorResolver(tagHelpers); + } + + public RazorEngine Engine { get; set; } + + public ITagHelperDescriptorResolver Resolver { get; } + } + + private class TagHelperDescriptorResolver : ITagHelperDescriptorResolver + { + public TagHelperDescriptorResolver(TagHelperDescriptor[] tagHelpers) + { + TagHelpers = tagHelpers; + } + + public TagHelperDescriptor[] TagHelpers { get; } + + public IEnumerable Resolve(ErrorSink errorSink) + { + return TagHelpers; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/MvcTagHelperAttributeValueCodeRendererTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/MvcTagHelperAttributeValueCodeRendererTest.cs deleted file mode 100644 index 7d873eba3e..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/MvcTagHelperAttributeValueCodeRendererTest.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Chunks.Generators; -using Microsoft.AspNetCore.Razor.CodeGenerators; -using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Razor -{ - public class MvcTagHelperAttributeValueCodeRendererTest - { - #if OLD_RAZOR - [Theory] - [InlineData("SomeType", "SomeType", "Provider.SomeMethod(ViewData, __model => __model.MyValue)")] - [InlineData("SomeType", "SomeType2", "MyValue")] - public void RenderAttributeValue_RendersModelExpressionsCorrectly( - string modelExpressionType, - string propertyType, - string expectedValue) - { - // Arrange - var renderer = new MvcTagHelperAttributeValueCodeRenderer( - new GeneratedTagHelperAttributeContext - { - ModelExpressionTypeName = modelExpressionType, - CreateModelExpressionMethodName = "SomeMethod", - ModelExpressionProviderPropertyName = "Provider", - ViewDataPropertyName = "ViewData" - }); - var attributeDescriptor = new TagHelperAttributeDescriptor - { - Name = "MyAttribute", - PropertyName = "SomeProperty", - TypeName = propertyType, - }; - var writer = new CSharpCodeWriter(); - var generatorContext = new ChunkGeneratorContext( - host: null, - className: string.Empty, - rootNamespace: string.Empty, - sourceFile: string.Empty, - shouldGenerateLinePragmas: true); - var errorSink = new ErrorSink(); - var context = new CodeGeneratorContext(generatorContext, errorSink); - - // Act - renderer.RenderAttributeValue(attributeDescriptor, writer, context, - (codeWriter) => - { - codeWriter.Write("MyValue"); - }, - complexValue: false); - - // Assert - Assert.Equal(expectedValue, writer.GenerateCode()); - } -#endif - } -} \ No newline at end of file