diff --git a/src/Microsoft.AspNet.Razor.Runtime/AssemblyInfo.cs b/src/Microsoft.AspNet.Razor.Runtime/AssemblyInfo.cs new file mode 100644 index 0000000000..d36bab93fd --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime/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.Razor.Runtime.Test")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/NotNullArgument.cs b/src/Microsoft.AspNet.Razor.Runtime/NotNullArgument.cs new file mode 100644 index 0000000000..4b9755cce9 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime/NotNullArgument.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.AspNet.Razor.Runtime +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..5a0fccdfdb --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs @@ -0,0 +1,82 @@ +// +namespace Microsoft.AspNet.Razor.Runtime +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Razor.Runtime.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// Invalid tag helper directive look up text '{0}'. The correct look up text formats are: + /// "assemblyName" + /// "typeName, assemblyName" + /// + internal static string TagHelperDescriptorResolver_InvalidTagHelperLookupText + { + get { return GetString("TagHelperDescriptorResolver_InvalidTagHelperLookupText"); } + } + + /// + /// Invalid tag helper directive look up text '{0}'. The correct look up text formats are: + /// "assemblyName" + /// "typeName, assemblyName" + /// + internal static string FormatTagHelperDescriptorResolver_InvalidTagHelperLookupText(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorResolver_InvalidTagHelperLookupText"), p0); + } + + /// + /// Cannot resolve TagHelper containing assembly '{0}'. Error: '{1}'. + /// + internal static string TagHelperTypeResolver_CannotResolveTagHelperAssembly + { + get { return GetString("TagHelperTypeResolver_CannotResolveTagHelperAssembly"); } + } + + /// + /// Cannot resolve TagHelper containing assembly '{0}'. Error: '{1}'. + /// + internal static string FormatTagHelperTypeResolver_CannotResolveTagHelperAssembly(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperTypeResolver_CannotResolveTagHelperAssembly"), p0, p1); + } + + /// + /// Tag helper directive assembly name cannot be null or empty. + /// + internal static string TagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull + { + get { return GetString("TagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull"); } + } + + /// + /// Tag helper directive assembly name cannot be null or empty. + /// + internal static string FormatTagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull() + { + return GetString("TagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull"); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx new file mode 100644 index 0000000000..8a7d783249 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Invalid tag helper directive look up text '{0}'. The correct look up text formats are: +"assemblyName" +"typeName, assemblyName" + + + Cannot resolve TagHelper containing assembly '{0}'. Error: '{1}'. + + + Tag helper directive assembly name cannot be null or empty. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/ITagHelper.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/ITagHelper.cs new file mode 100644 index 0000000000..badb7227e0 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/ITagHelper.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + public interface ITagHelper + { + // TODO: Will be implemented in https://github.com/aspnet/razor/issues/154 + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs new file mode 100644 index 0000000000..c7813265ca --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.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.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Factory for s from s. + /// + public static class TagHelperDescriptorFactory + { + private const string TagHelperNameEnding = "TagHelper"; + + // TODO: Investigate if we should cache TagHelperDescriptors for types: + // https://github.com/aspnet/Razor/issues/165 + + /// + /// Creates a from the given . + /// + /// The type to create a from. + /// A that describes the given . + public static TagHelperDescriptor CreateDescriptor(Type type) + { + var tagName = GetTagName(type); + var typeName = type.FullName; + var attributeDescriptors = GetAttributeDescriptors(type); + var contentBehavior = GetContentBehavior(type); + + return new TagHelperDescriptor(tagName, + typeName, + contentBehavior, + attributeDescriptors); + } + + // TODO: Make this method support TagNameAttribute tag names: https://github.com/aspnet/Razor/issues/120 + private static string GetTagName(Type tagHelperType) + { + var name = tagHelperType.Name; + + if (name.EndsWith(TagHelperNameEnding, StringComparison.OrdinalIgnoreCase)) + { + name = name.Substring(0, name.Length - TagHelperNameEnding.Length); + } + + return name; + } + + private static IEnumerable GetAttributeDescriptors(Type type) + { + var typeInfo = type.GetTypeInfo(); + var properties = typeInfo.DeclaredProperties.Where(IsValidProperty); + var attributeDescriptors = properties.Select(ToAttributeDescriptor); + + return attributeDescriptors; + } + + // TODO: Make the HTML attribute name support names from a AttributeNameAttribute: + // https://github.com/aspnet/Razor/issues/121 + private static TagHelperAttributeDescriptor ToAttributeDescriptor(PropertyInfo property) + { + return new TagHelperAttributeDescriptor(property.Name, property); + } + + // TODO: Make the content behavior pull from a ContentBehaviorAttribute: https://github.com/aspnet/Razor/issues/122 + private static ContentBehavior GetContentBehavior(Type type) + { + return ContentBehavior.None; + } + + private static bool IsValidProperty(PropertyInfo property) + { + return property.GetMethod != null && + property.GetMethod.IsPublic && + property.SetMethod != null && + property.SetMethod.IsPublic; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs new file mode 100644 index 0000000000..3cd8a5c2c9 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs @@ -0,0 +1,78 @@ +// 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.TagHelpers; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Used to resolve s. + /// + public class TagHelperDescriptorResolver : ITagHelperDescriptorResolver + { + private readonly TagHelperTypeResolver _typeResolver; + + // internal for testing + internal TagHelperDescriptorResolver(TagHelperTypeResolver typeResolver) + { + _typeResolver = typeResolver; + } + + /// + /// Instantiates a new instance of the class. + /// + public TagHelperDescriptorResolver() + : this(new TagHelperTypeResolver()) + { + } + + /// + public IEnumerable Resolve(string lookupText) + { + var lookupStrings = lookupText?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + // Ensure that we have valid lookupStrings to work with. Valid formats are: + // "assemblyName" + // "typeName, assemblyName" + if (string.IsNullOrEmpty(lookupText) || + (lookupStrings.Length != 1 && lookupStrings.Length != 2)) + { + throw new ArgumentException( + Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperLookupText(lookupText), + nameof(lookupText)); + } + + var tagHelperTypes = ResolveTagHelperTypes(lookupStrings); + + // Convert types to TagHelperDescriptors + var descriptors = tagHelperTypes.Select(TagHelperDescriptorFactory.CreateDescriptor); + + return descriptors; + } + + private IEnumerable ResolveTagHelperTypes(string[] lookupStrings) + { + // Grab the assembly name from the lookup text strings. Due to our supported lookupText formats it will + // always be the last element provided. + var assemblyName = lookupStrings.Last().Trim(); + + // Resolve valid tag helper types from the assembly. + var types = _typeResolver.Resolve(assemblyName); + + // Check if the lookupText specifies a type to search for. + if (lookupStrings.Length == 2) + { + // The user provided a type name retrieve the value and trim it. + var typeName = lookupStrings[0].Trim(); + + types = types.Where(type => + string.Equals(type.Namespace + "." + type.Name, typeName, StringComparison.Ordinal)); + } + + return types; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperTypeResolver.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperTypeResolver.cs new file mode 100644 index 0000000000..ca20041aa1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperTypeResolver.cs @@ -0,0 +1,74 @@ +// 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 System.Reflection; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Class that locates valid s within an assembly. + /// + public class TagHelperTypeResolver + { + private static readonly TypeInfo ITagHelperTypeInfo = typeof(ITagHelper).GetTypeInfo(); + + /// + /// Instantiates a new instance of the class. + /// + public TagHelperTypeResolver() + { + } + + /// + /// Loads an using the given and resolves + /// all valid s. + /// + /// The name of an to search. + /// An of valid s. + public IEnumerable Resolve(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException( + Resources.TagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull, + nameof(name)); + } + + var assemblyName = new AssemblyName(name); + var libraryTypes = GetLibraryDefinedTypes(assemblyName); + var validTagHelpers = libraryTypes.Where(IsTagHelper); + + // Convert from TypeInfo[] to Type[] + return validTagHelpers.Select(type => type.AsType()); + } + + // Internal for testing, don't want to be loading assemblies during a test. + internal virtual IEnumerable GetLibraryDefinedTypes(AssemblyName assemblyName) + { + try + { + var assembly = Assembly.Load(assemblyName); + + return assembly.DefinedTypes; + } + catch (Exception ex) + { + throw new InvalidOperationException( + Resources.FormatTagHelperTypeResolver_CannotResolveTagHelperAssembly(assemblyName.Name, + ex.Message)); + } + } + + private static bool IsTagHelper(TypeInfo typeInfo) + { + return typeInfo.IsPublic && + !typeInfo.IsAbstract && + !typeInfo.IsGenericType && + !typeInfo.IsNested && + ITagHelperTypeInfo.IsAssignableFrom(typeInfo); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperRegistrationVisitor.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperRegistrationVisitor.cs index b59342e39d..3681f85f01 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperRegistrationVisitor.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperRegistrationVisitor.cs @@ -40,7 +40,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal throw new InvalidOperationException( RazorResources.FormatTagHelpers_CannotUseDirectiveWithNoTagHelperDescriptorResolver( SyntaxConstants.CSharp.AddTagHelperKeyword, - nameof(TagHelperDescriptorResolver), + nameof(ITagHelperDescriptorResolver), nameof(RazorParser))); } diff --git a/src/Microsoft.AspNet.Razor/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Razor/Properties/AssemblyInfo.cs index e433d6ddfa..e4276f6d3c 100644 --- a/src/Microsoft.AspNet.Razor/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNet.Razor/Properties/AssemblyInfo.cs @@ -3,4 +3,5 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Microsoft.AspNet.Razor.Test")] \ No newline at end of file +[assembly: InternalsVisibleTo("Microsoft.AspNet.Razor.Test")] +[assembly: InternalsVisibleTo("Microsoft.AspNet.Razor.Runtime.Test")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/RazorEngineHost.cs b/src/Microsoft.AspNet.Razor/RazorEngineHost.cs index ca38205e8c..8953e8d132 100644 --- a/src/Microsoft.AspNet.Razor/RazorEngineHost.cs +++ b/src/Microsoft.AspNet.Razor/RazorEngineHost.cs @@ -113,6 +113,11 @@ namespace Microsoft.AspNet.Razor /// public virtual RazorCodeLanguage CodeLanguage { get; protected set; } + /// + /// The used to resolve s. + /// + public virtual ITagHelperDescriptorResolver TagHelperDescriptorResolver { get; protected set; } + /// /// Boolean indicating if instrumentation code should be injected into the output page /// @@ -161,12 +166,6 @@ namespace Microsoft.AspNet.Razor return null; } - // TODO: Document this as part of https://github.com/aspnet/Razor/issues/99 - public virtual ITagHelperDescriptorResolver CreateTagHelperDescriptorResolver() - { - return new TagHelperDescriptorResolver(); - } - /// /// Gets an instance of the code parser and is provided an opportunity to decorate or replace it /// diff --git a/src/Microsoft.AspNet.Razor/RazorTemplateEngine.cs b/src/Microsoft.AspNet.Razor/RazorTemplateEngine.cs index 5bb72bba22..548107c142 100644 --- a/src/Microsoft.AspNet.Razor/RazorTemplateEngine.cs +++ b/src/Microsoft.AspNet.Razor/RazorTemplateEngine.cs @@ -271,11 +271,10 @@ namespace Microsoft.AspNet.Razor { var codeParser = Host.CodeLanguage.CreateCodeParser(); var markupParser = Host.CreateMarkupParser(); - var tagHelperDescriptorResolver = Host.CreateTagHelperDescriptorResolver(); return new RazorParser(Host.DecorateCodeParser(codeParser), Host.DecorateMarkupParser(markupParser), - tagHelperDescriptorResolver) + Host.TagHelperDescriptorResolver) { DesignTimeMode = Host.DesignTimeMode }; diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/ITagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Razor/TagHelpers/ITagHelperDescriptorResolver.cs index a479278f09..747242a8a0 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/ITagHelperDescriptorResolver.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/ITagHelperDescriptorResolver.cs @@ -5,10 +5,19 @@ using System.Collections.Generic; namespace Microsoft.AspNet.Razor.TagHelpers { - // TODO: Document this class as part of https://github.com/aspnet/Razor/issues/99 - + /// + /// Contract used to resolve s. + /// public interface ITagHelperDescriptorResolver { + /// + /// Resolves s matching the given . + /// + /// + /// A used to find tag helper s. + /// + /// An of s matching + /// the given . IEnumerable Resolve(string lookupText); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs index 06b1fce0e5..bca5f415ce 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs @@ -8,7 +8,7 @@ using Microsoft.Internal.Web.Utils; namespace Microsoft.AspNet.Razor.TagHelpers { /// - /// Defines a an that is used to check equality between + /// An used to check equality between /// two s. /// public class TagHelperDescriptorComparer : IEqualityComparer @@ -18,7 +18,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// public static readonly TagHelperDescriptorComparer Default = new TagHelperDescriptorComparer(); - private TagHelperDescriptorComparer() + // Internal for testing + internal TagHelperDescriptorComparer() { } diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorResolver.cs deleted file mode 100644 index 9b8f5901c7..0000000000 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorResolver.cs +++ /dev/null @@ -1,18 +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; -using System.Linq; - -namespace Microsoft.AspNet.Razor.TagHelpers -{ - // TODO: Implement this class as part of https://github.com/aspnet/Razor/issues/99 - - public class TagHelperDescriptorResolver : ITagHelperDescriptorResolver - { - public IEnumerable Resolve(string lookupText) - { - return Enumerable.Empty(); - } - } -} \ No newline at end of file