diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs index 942e0084a5..ed73c0b122 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs @@ -157,7 +157,11 @@ internal CompilationResult GetCompilationFailedResult( sourceFileContent = ReadFileContentsSafely(_fileProvider, sourceFilePath); } - var compilationFailure = new CompilationFailure(sourceFilePath, sourceFileContent, compilationContent, group.Select(d => d.ToDiagnosticMessage(_environment.RuntimeFramework))); + var compilationFailure = new CompilationFailure( + sourceFilePath, + sourceFileContent, + compilationContent, + group.Select(d => d.ToDiagnosticMessage(_environment.RuntimeFramework))); failures.Add(compilationFailure); } @@ -176,13 +180,25 @@ private static string GetFilePath(string relativePath, Diagnostic diagnostic) } private List GetApplicationReferences() + { + return GetApplicationReferences( + _metadataFileCache, + _libraryExporter, + _environment.ApplicationName); + } + + // Internal for unit testing + internal static List GetApplicationReferences( + ConcurrentDictionary metadataFileCache, + ILibraryExporter libraryExporter, + string applicationName) { var references = new List(); // Get the MetadataReference for the executing application. If it's a Roslyn reference, // we can copy the references created when compiling the application to the Razor page being compiled. // This avoids performing expensive calls to MetadataReference.CreateFromImage. - var libraryExport = _libraryExporter.GetExport(_environment.ApplicationName); + var libraryExport = libraryExporter.GetExport(applicationName); if (libraryExport?.MetadataReferences != null && libraryExport.MetadataReferences.Count > 0) { Debug.Assert(libraryExport.MetadataReferences.Count == 1, @@ -197,20 +213,22 @@ private List GetApplicationReferences() } } - var export = _libraryExporter.GetAllExports(_environment.ApplicationName); + var export = libraryExporter.GetAllExports(applicationName); foreach (var metadataReference in export.MetadataReferences) { // Taken from https://github.com/aspnet/KRuntime/blob/757ba9bfdf80bd6277e715d6375969a7f44370ee/src/... // Microsoft.Framework.Runtime.Roslyn/RoslynCompiler.cs#L164 // We don't want to take a dependency on the Roslyn bit directly since it pulls in more dependencies // than the view engine needs (Microsoft.Framework.Runtime) for example - references.Add(ConvertMetadataReference(metadataReference)); + references.Add(ConvertMetadataReference(metadataFileCache, metadataReference)); } return references; } - private MetadataReference ConvertMetadataReference(IMetadataReference metadataReference) + private static MetadataReference ConvertMetadataReference( + ConcurrentDictionary metadataFileCache, + IMetadataReference metadataReference) { var roslynReference = metadataReference as IRoslynMetadataReference; @@ -230,7 +248,7 @@ private MetadataReference ConvertMetadataReference(IMetadataReference metadataRe if (fileMetadataReference != null) { - return CreateMetadataFileReference(fileMetadataReference.Path); + return CreateMetadataFileReference(metadataFileCache, fileMetadataReference.Path); } var projectReference = metadataReference as IMetadataProjectReference; @@ -247,9 +265,11 @@ private MetadataReference ConvertMetadataReference(IMetadataReference metadataRe throw new NotSupportedException(); } - private MetadataReference CreateMetadataFileReference(string path) + private static MetadataReference CreateMetadataFileReference( + ConcurrentDictionary metadataFileCache, + string path) { - var metadata = _metadataFileCache.GetOrAdd(path, _ => + var metadata = metadataFileCache.GetOrAdd(path, _ => { using (var stream = File.OpenRead(path)) { diff --git a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisAttributeUtilities.cs b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisAttributeUtilities.cs new file mode 100644 index 0000000000..ac3922202b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisAttributeUtilities.cs @@ -0,0 +1,256 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + /// + /// Utilities to work with creating instances from . + /// + public static class CodeAnalysisAttributeUtilities + { + private static readonly ConcurrentDictionary> _constructorCache = + new ConcurrentDictionary>(); + + /// + /// Gets the sequence of s of type + /// that are declared on the specified . + /// + /// The type. + /// The to find attributes on. + /// The . + /// + public static IEnumerable GetCustomAttributes( + ISymbol symbol, + CodeAnalysisSymbolLookupCache symbolLookup) + where TAttribute : Attribute + { + var attributes = symbol.GetAttributes(); + if (attributes.Length > 0) + { + var attributeSymbol = symbolLookup.GetSymbol(typeof(TAttribute).GetTypeInfo()); + return attributes + .Where(attribute => attribute.AttributeClass == attributeSymbol) + .Select(attribute => CreateAttribute(symbolLookup, attribute)) + .ToArray(); + } + + return Enumerable.Empty(); + } + + private static TAttribute CreateAttribute( + CodeAnalysisSymbolLookupCache symbolLookup, + AttributeData attributeData) + where TAttribute : Attribute + { + TAttribute attribute; + if (attributeData.ConstructorArguments.Length == 0) + { + attribute = Activator.CreateInstance(); + } + else + { + var matchInfo = MatchConstructor(typeof(TAttribute), attributeData, symbolLookup); + Func constructorDelegate; + if (!_constructorCache.TryGetValue(matchInfo.Constructor, out constructorDelegate)) + { + constructorDelegate = MakeFastConstructorInvoker(matchInfo); + _constructorCache[matchInfo.Constructor] = constructorDelegate; + } + + attribute = (TAttribute)constructorDelegate(matchInfo.ArgumentValues); + } + + if (attributeData.NamedArguments.Length > 0) + { + var helpers = PropertyHelper.GetVisibleProperties(attribute); + foreach (var item in attributeData.NamedArguments) + { + var helper = helpers.FirstOrDefault( + propertyHelper => string.Equals(propertyHelper.Name, item.Key, StringComparison.Ordinal)); + + if (helper == null) + { + throw new InvalidOperationException( + Resources.FormatCodeAnalysis_PropertyNotFound(item.Key, attribute.GetType().FullName)); + } + + var propertyValue = ConvertTypedConstantValue( + helper.Property.PropertyType, + item.Value, + symbolLookup); + helper.SetValue(attribute, propertyValue); + } + } + + return attribute; + } + + private static Func MakeFastConstructorInvoker(ConstructorMatchInfo matchInfo) + { + var argsParameter = Expression.Parameter(typeof(object[]), "args"); + var parameters = new Expression[matchInfo.ArgumentValues.Length]; + + for (var index = 0; index < matchInfo.ArgumentValues.Length; index++) + { + parameters[index] = + Expression.Convert( + Expression.ArrayIndex( + argsParameter, + Expression.Constant(index)), + matchInfo.ArgumentValues[index].GetType()); + } + + + // () => new TAttribute(args) + var lambda = + Expression.Lambda>( + Expression.New( + matchInfo.Constructor, + parameters), + argsParameter); + + return lambda.Compile(); + } + + private static ConstructorMatchInfo MatchConstructor( + Type type, + AttributeData attributeData, + CodeAnalysisSymbolLookupCache symbolLookup) + { + var constructor = FindConstructor(type, attributeData.ConstructorArguments, symbolLookup); + var constructorParmaters = constructor.GetParameters(); + + var arguments = new object[attributeData.ConstructorArguments.Length]; + for (var i = 0; i < arguments.Length; i++) + { + var value = ConvertTypedConstantValue( + constructorParmaters[i].ParameterType, + attributeData.ConstructorArguments[i], + symbolLookup); + + arguments[i] = value; + } + + return new ConstructorMatchInfo + { + Constructor = constructor, + ArgumentValues = arguments + }; + } + + private static ConstructorInfo FindConstructor( + Type type, + ImmutableArray symbolConstructorArguments, + CodeAnalysisSymbolLookupCache symbolLookup) + { + var constructors = type.GetConstructors(); + // One or more constructor arguments were specified. Consequently, + // we must have at least one constructor that matches. + Debug.Assert(constructors.Length != 0); + + if (constructors.Length == 1) + { + return constructors[0]; + } + + foreach (var constructor in constructors) + { + var runtimeParameters = constructor.GetParameters(); + if (runtimeParameters.Length != symbolConstructorArguments.Length) + { + continue; + } + + var parametersMatched = true; + for (var index = 0; index < runtimeParameters.Length; index++) + { + var runtimeParameter = runtimeParameters[index].ParameterType; + if (symbolConstructorArguments[index].Kind == TypedConstantKind.Array && + runtimeParameter.IsArray) + { + var arrayType = (IArrayTypeSymbol)symbolConstructorArguments[index].Type; + if (symbolLookup.GetSymbol(runtimeParameter.GetElementType().GetTypeInfo()) != + arrayType.ElementType) + { + parametersMatched = false; + break; + } + } + else + { + var parameterSymbol = symbolLookup.GetSymbol(runtimeParameter.GetTypeInfo()); + if (symbolConstructorArguments[index].Type != parameterSymbol) + { + parametersMatched = false; + break; + } + } + } + + if (parametersMatched) + { + return constructor; + } + } + + throw new InvalidOperationException(Resources.FormatCodeAnalysisConstructorNotFound(type.FullName)); + } + + private static object ConvertTypedConstantValue( + Type type, + TypedConstant constructorArgument, + CodeAnalysisSymbolLookupCache symbolLookup) + { + object value; + switch (constructorArgument.Kind) + { + case TypedConstantKind.Enum: + value = Enum.ToObject(type, constructorArgument.Value); + break; + case TypedConstantKind.Primitive: + value = constructorArgument.Value; + break; + case TypedConstantKind.Type: + var typeSymbol = (INamedTypeSymbol)constructorArgument.Value; + var typeName = CodeAnalysisSymbolBasedTypeInfo.GetAssemblyQualifiedName(typeSymbol); + value = Type.GetType(typeName); + break; + case TypedConstantKind.Array: + Debug.Assert(type.IsArray && constructorArgument.Values != null); + var elementType = type.GetElementType(); + var values = Array.CreateInstance(elementType, constructorArgument.Values.Length); + for (var index = 0; index < values.Length; index++) + { + values.SetValue( + ConvertTypedConstantValue(elementType, constructorArgument.Values[index], symbolLookup), + index); + } + value = values; + break; + default: + throw new NotSupportedException( + Resources.FormatCodeAnalysis_TypeConstantKindNotSupported(constructorArgument.Kind)); + } + + return value; + } + + private struct ConstructorMatchInfo + { + public ConstructorInfo Constructor; + + public object[] ArgumentValues; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisSymbolBasedPropertyInfo.cs b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisSymbolBasedPropertyInfo.cs new file mode 100644 index 0000000000..241ff048f1 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisSymbolBasedPropertyInfo.cs @@ -0,0 +1,69 @@ +// 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.Diagnostics; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.CodeAnalysis; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + /// + /// implementation using Code Analysis symbols. + /// + [DebuggerDisplay("{Name, PropertyType}")] + public class CodeAnalysisSymbolBasedPropertyInfo : IPropertyInfo + { + private readonly IPropertySymbol _propertySymbol; + private readonly CodeAnalysisSymbolLookupCache _symbolLookup; + + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + public CodeAnalysisSymbolBasedPropertyInfo( + [NotNull] IPropertySymbol propertySymbol, + [NotNull] CodeAnalysisSymbolLookupCache symbolLookup) + { + _symbolLookup = symbolLookup; + _propertySymbol = propertySymbol; + PropertyType = new CodeAnalysisSymbolBasedTypeInfo(_propertySymbol.Type, _symbolLookup); + } + + /// + public bool HasPublicGetter + { + get + { + return _propertySymbol.GetMethod != null && + _propertySymbol.GetMethod.DeclaredAccessibility == Accessibility.Public; + } + } + + /// + public bool HasPublicSetter + { + get + { + return _propertySymbol.SetMethod != null && + _propertySymbol.SetMethod.DeclaredAccessibility == Accessibility.Public; + } + } + + /// + public string Name => _propertySymbol.MetadataName; + + /// + public ITypeInfo PropertyType { get; } + + /// + public IEnumerable GetCustomAttributes() + where TAttribute : Attribute + { + return CodeAnalysisAttributeUtilities.GetCustomAttributes(_propertySymbol, _symbolLookup); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisSymbolBasedTypeInfo.cs b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisSymbolBasedTypeInfo.cs new file mode 100644 index 0000000000..9ead141d2b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisSymbolBasedTypeInfo.cs @@ -0,0 +1,246 @@ +// 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.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.CodeAnalysis; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + /// + /// implementation using Code Analysis symbols. + /// + [DebuggerDisplay("{Name}")] + public class CodeAnalysisSymbolBasedTypeInfo : ITypeInfo + { + private static readonly System.Reflection.TypeInfo OpenGenericDictionaryTypeInfo = + typeof(IDictionary<,>).GetTypeInfo(); + private readonly CodeAnalysisSymbolLookupCache _symbolLookup; + private readonly ITypeSymbol _type; + private string _fullName; + private List _properties; + + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + public CodeAnalysisSymbolBasedTypeInfo( + [NotNull] ITypeSymbol type, + [NotNull] CodeAnalysisSymbolLookupCache symbolLookup) + { + _symbolLookup = symbolLookup; + _type = type; + } + + /// + public string FullName + { + get + { + if (_fullName == null) + { + _fullName = GetFullName(_type); + } + + return _fullName; + } + } + + /// + public bool IsAbstract => _type.IsAbstract; + + /// + public bool IsGenericType + { + get + { + return _type.Kind == SymbolKind.NamedType && + ((INamedTypeSymbol)_type).IsGenericType; + } + } + + /// + public bool IsNested => _type.ContainingType != null; + + /// + public bool IsPublic => _type.DeclaredAccessibility == Accessibility.Public; + + /// + public string Name => _type.MetadataName; + + /// + public IEnumerable Properties + { + get + { + if (_properties == null) + { + _properties = PopulateProperties(); + } + + return _properties; + } + } + + /// + public IEnumerable GetCustomAttributes() + where TAttribute : Attribute + { + return CodeAnalysisAttributeUtilities.GetCustomAttributes(_type, _symbolLookup); + } + + /// + public bool ImplementsInterface(System.Reflection.TypeInfo interfaceType) + { + var interfaceSymbol = _symbolLookup.GetSymbol(interfaceType); + return _type.AllInterfaces.Any(implementedInterface => implementedInterface == interfaceSymbol); + } + + /// + public string[] GetGenericDictionaryParameterNames() + { + var dictionarySymbol = _symbolLookup.GetSymbol(OpenGenericDictionaryTypeInfo); + + INamedTypeSymbol dictionaryInterface; + if (_type.Kind == SymbolKind.NamedType && + ((INamedTypeSymbol)_type).ConstructedFrom == dictionarySymbol) + { + dictionaryInterface = (INamedTypeSymbol)_type; + } + else + { + dictionaryInterface = _type + .AllInterfaces + .FirstOrDefault(implementedInterface => implementedInterface.ConstructedFrom == dictionarySymbol); + } + + if (dictionaryInterface != null) + { + Debug.Assert(dictionaryInterface.TypeArguments.Length == 2); + + return new[] + { + GetFullName(dictionaryInterface.TypeArguments[0]), + GetFullName(dictionaryInterface.TypeArguments[1]) + }; + } + + return null; + } + + public static string GetAssemblyQualifiedName(ITypeSymbol symbol) + { + var builder = new StringBuilder(); + GetAssemblyQualifiedName(builder, symbol); + + return builder.ToString(); + } + + private List PopulateProperties() + { + var properties = new List(); + var overridenProperties = new HashSet(); + var type = _type; + + while (type != null) + { + foreach (var member in type.GetMembers()) + { + if (member.Kind == SymbolKind.Property) + { + var propertySymbol = (IPropertySymbol)member; + if (!propertySymbol.IsIndexer && !overridenProperties.Contains(propertySymbol)) + { + var propertyInfo = new CodeAnalysisSymbolBasedPropertyInfo(propertySymbol, _symbolLookup); + properties.Add(propertyInfo); + + if (propertySymbol.IsOverride) + { + overridenProperties.Add(propertySymbol.OverriddenProperty); + } + } + } + } + + type = type.BaseType; + } + + return properties; + } + + private static string GetFullName(ITypeSymbol typeSymbol) + { + var nameBuilder = new StringBuilder(); + GetFullName(nameBuilder, typeSymbol); + + return nameBuilder.Length == 0 ? null : nameBuilder.ToString(); + } + + private static void GetFullName(StringBuilder nameBuilder, ITypeSymbol typeSymbol) + { + if (typeSymbol.Kind == SymbolKind.TypeParameter) + { + return; + } + + var insertIndex = nameBuilder.Length; + nameBuilder.Append(typeSymbol.MetadataName); + if (typeSymbol.Kind == SymbolKind.NamedType) + { + var namedSymbol = (INamedTypeSymbol)typeSymbol; + // The symbol represents a generic but not open generic type + if (namedSymbol.IsGenericType && + namedSymbol.ConstructedFrom != namedSymbol) + { + nameBuilder.Append('['); + foreach (var typeArgument in namedSymbol.TypeArguments) + { + nameBuilder.Append('['); + GetAssemblyQualifiedName(nameBuilder, typeArgument); + nameBuilder + .Append(']') + .Append(','); + } + + nameBuilder.Length--; + nameBuilder.Append("]"); + } + } + + var containingType = typeSymbol.ContainingType; + while (containingType != null) + { + nameBuilder + .Insert(insertIndex, '+') + .Insert(insertIndex, containingType.MetadataName); + + containingType = containingType.ContainingType; + } + + var containingNamespace = typeSymbol.ContainingNamespace; + while (!containingNamespace.IsGlobalNamespace) + { + nameBuilder + .Insert(insertIndex, '.') + .Insert(insertIndex, containingNamespace.MetadataName); + + containingNamespace = containingNamespace.ContainingNamespace; + } + } + + private static void GetAssemblyQualifiedName(StringBuilder builder, ITypeSymbol typeSymbol) + { + GetFullName(builder, typeSymbol); + builder + .Append(", ") + .Append(typeSymbol.ContainingAssembly.Identity); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisSymbolLookupCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisSymbolLookupCache.cs new file mode 100644 index 0000000000..f6748217e4 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/CodeAnalysisSymbolLookupCache.cs @@ -0,0 +1,49 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + /// + /// Caches mapping of to . + /// + public class CodeAnalysisSymbolLookupCache + { + private readonly Dictionary _symbolLookup = + new Dictionary(); + private readonly object _lookupLock = new object(); + private readonly CodeAnalysis.Compilation _compilation; + + /// + /// Initialzes a new instance of . + /// + /// The instance. + public CodeAnalysisSymbolLookupCache([NotNull] CodeAnalysis.Compilation compilation) + { + _compilation = compilation; + } + + /// + /// Gets a that corresponds to . + /// + /// The to lookup. + /// + public INamedTypeSymbol GetSymbol([NotNull] System.Reflection.TypeInfo typeInfo) + { + lock (_lookupLock) + { + INamedTypeSymbol typeSymbol; + if (!_symbolLookup.TryGetValue(typeInfo, out typeSymbol)) + { + typeSymbol = _compilation.GetTypeByMetadataName(typeInfo.FullName); + _symbolLookup[typeInfo] = typeSymbol; + } + + return typeSymbol; + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/PrecompilationTagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/PrecompilationTagHelperDescriptorResolver.cs deleted file mode 100644 index d794225af7..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/PrecompilationTagHelperDescriptorResolver.cs +++ /dev/null @@ -1,88 +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 System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using Microsoft.AspNet.Razor.Runtime.TagHelpers; -using Microsoft.Dnx.Compilation.CSharp; -using Microsoft.Dnx.Runtime; -using Microsoft.Framework.Internal; - -namespace Microsoft.AspNet.Mvc.Razor.Precompilation -{ - /// - /// used during Razor precompilation. - /// - public class PrecompilationTagHelperTypeResolver : TagHelperTypeResolver - { - private static readonly string TagHelperTypeName = typeof(ITagHelper).FullName; - private readonly BeforeCompileContext _compileContext; - private readonly IAssemblyLoadContext _loadContext; - private object _compilationLock = new object(); - private bool _assemblyEmited; - private TypeInfo[] _exportedTypeInfos; - - /// - /// Initializes a new instance of . - /// - /// The . - /// The . - public PrecompilationTagHelperTypeResolver([NotNull] BeforeCompileContext compileContext, - [NotNull] IAssemblyLoadContext loadContext) - { - _compileContext = compileContext; - _loadContext = loadContext; - } - - /// - protected override IEnumerable GetExportedTypes([NotNull] AssemblyName assemblyName) - { - var compilingAssemblyName = _compileContext.Compilation.AssemblyName; - if (string.Equals(assemblyName.Name, compilingAssemblyName, StringComparison.Ordinal)) - { - return LazyInitializer.EnsureInitialized(ref _exportedTypeInfos, - ref _assemblyEmited, - ref _compilationLock, - GetExportedTypesFromCompilation); - } - - return GetExportedTypesCore(assemblyName); - } - - private IEnumerable GetExportedTypesCore(AssemblyName assemblyName) - { - var assembly = _loadContext.Load(assemblyName.Name); - - return assembly.ExportedTypes.Select(type => type.GetTypeInfo()); - } - - private TypeInfo[] GetExportedTypesFromCompilation() - { - using (var stream = new MemoryStream()) - { - var assemblyName = string.Join(".", _compileContext.Compilation.AssemblyName, - nameof(PrecompilationTagHelperTypeResolver), - Path.GetRandomFileName()); - - var emitResult = _compileContext.Compilation - .WithAssemblyName(assemblyName) - .Emit(stream); - if (!emitResult.Success) - { - // Return an empty sequence. Compilation will fail once precompilation completes. - return new TypeInfo[0]; - } - - stream.Position = 0; - var assembly = _loadContext.LoadStream(stream, assemblySymbols: null); - return assembly.ExportedTypes - .Select(type => type.GetTypeInfo()) - .ToArray(); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/PrecompilationTagHelperTypeResolver.cs b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/PrecompilationTagHelperTypeResolver.cs new file mode 100644 index 0000000000..7fe7904633 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/PrecompilationTagHelperTypeResolver.cs @@ -0,0 +1,109 @@ +// 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.Reflection; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.CodeAnalysis; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + /// + /// used during Razor precompilation. + /// + public class PrecompilationTagHelperTypeResolver : TagHelperTypeResolver + { + private readonly object _assemblyLookupLock = new object(); + private readonly Dictionary> _assemblyLookup + = new Dictionary>(StringComparer.OrdinalIgnoreCase); + private readonly CodeAnalysis.Compilation _compilation; + private readonly CodeAnalysisSymbolLookupCache _symbolLookup; + + /// + /// Initializes a new instance of . + /// + /// The . + public PrecompilationTagHelperTypeResolver([NotNull] CodeAnalysis.Compilation compilation) + { + _compilation = compilation; + _symbolLookup = new CodeAnalysisSymbolLookupCache(compilation); + } + + /// + protected override IEnumerable GetTopLevelExportedTypes([NotNull] AssemblyName assemblyName) + { + lock (_assemblyLookupLock) + { + IEnumerable result; + if (!_assemblyLookup.TryGetValue(assemblyName.Name, out result)) + { + result = GetExportedTypes(assemblyName.Name); + _assemblyLookup[assemblyName.Name] = result; + } + + return result; + } + } + + // Internal for unit testing + internal IEnumerable GetExportedTypes(string assemblyName) + { + if (string.Equals(_compilation.AssemblyName, assemblyName, StringComparison.Ordinal)) + { + return GetExportedTypes(_compilation.Assembly); + } + else + { + foreach (var reference in _compilation.References) + { + var compilationReference = reference as CompilationReference; + if (compilationReference != null && + string.Equals( + compilationReference.Compilation.AssemblyName, + assemblyName, + StringComparison.Ordinal)) + { + return GetExportedTypes(compilationReference.Compilation.Assembly); + } + + var assemblySymbol = _compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; + if (string.Equals( + assemblySymbol?.Identity.Name, + assemblyName, + StringComparison.Ordinal)) + { + return GetExportedTypes(assemblySymbol); + } + } + } + + throw new InvalidOperationException("Unable to load assembly reference '{0)'."); + } + + private List GetExportedTypes(IAssemblySymbol assembly) + { + var exportedTypes = new List(); + GetExportedTypes(assembly.GlobalNamespace, exportedTypes); + return exportedTypes; + } + + private void GetExportedTypes(INamespaceSymbol namespaceSymbol, List exportedTypes) + { + foreach (var type in namespaceSymbol.GetTypeMembers()) + { + if (type.TypeKind == TypeKind.Class && + type.DeclaredAccessibility == Accessibility.Public) + { + exportedTypes.Add(new CodeAnalysisSymbolBasedTypeInfo(type, _symbolLookup)); + } + } + + foreach (var subNamespace in namespaceSymbol.GetNamespaceMembers()) + { + GetExportedTypes(subNamespace, exportedTypes); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorPreCompiler.cs b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorPreCompiler.cs index c13b06357b..92a1f8ebb9 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorPreCompiler.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorPreCompiler.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.Mvc.Internal; @@ -41,7 +40,7 @@ public RazorPreCompiler( LanguageVersion = compileContext.Compilation.LanguageVersion }; PreCompilationCache = precompilationCache; - TagHelperTypeResolver = new PrecompilationTagHelperTypeResolver(CompileContext, LoadContext); + TagHelperTypeResolver = new PrecompilationTagHelperTypeResolver(compileContext.Compilation); } /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index a76d917405..360c586c17 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -446,6 +446,54 @@ internal static string FormatCouldNotResolveApplicationRelativeUrl_TagHelper(obj return string.Format(CultureInfo.CurrentCulture, GetString("CouldNotResolveApplicationRelativeUrl_TagHelper"), p0, p1, p2, p3, p4, p5); } + /// + /// Unable to find a suitable constructor for type'{0}'. + /// + internal static string CodeAnalysisConstructorNotFound + { + get { return GetString("CodeAnalysisConstructorNotFound"); } + } + + /// + /// Unable to find a suitable constructor for type'{0}'. + /// + internal static string FormatCodeAnalysisConstructorNotFound(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CodeAnalysisConstructorNotFound"), p0); + } + + /// + /// Unable to find property {0} on type {1}. + /// + internal static string CodeAnalysis_PropertyNotFound + { + get { return GetString("CodeAnalysis_PropertyNotFound"); } + } + + /// + /// Unable to find property {0} on type {1}. + /// + internal static string FormatCodeAnalysis_PropertyNotFound(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CodeAnalysis_PropertyNotFound"), p0, p1); + } + + /// + /// The type constant kind '{0}' is not supported. + /// + internal static string CodeAnalysis_TypeConstantKindNotSupported + { + get { return GetString("CodeAnalysis_TypeConstantKindNotSupported"); } + } + + /// + /// The type constant kind '{0}' is not supported. + /// + internal static string FormatCodeAnalysis_TypeConstantKindNotSupported(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CodeAnalysis_TypeConstantKindNotSupported"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index bd3dede0e4..644c65349c 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -200,4 +200,13 @@ @{3} "{4}, {5}" + + Unable to find a suitable constructor for type'{0}'. + + + Unable to find property {0} on type {1}. + + + The type constant kind '{0}' is not supported. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/project.json b/src/Microsoft.AspNet.Mvc.Razor/project.json index e798e0c7ca..1c6674a914 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/project.json +++ b/src/Microsoft.AspNet.Mvc.Razor/project.json @@ -24,6 +24,7 @@ "System.Collections": "", "System.IO": "", "System.Runtime": "", + "System.Reflection.Primitives": "4.0.0.0", "System.Text.Encoding": "", "System.Threading.Tasks": "" } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/AttributeWithArrayPropertiesAttribute.cs b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/AttributeWithArrayPropertiesAttribute.cs new file mode 100644 index 0000000000..c3ebe6e9d6 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/AttributeWithArrayPropertiesAttribute.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class AttributeWithArrayPropertiesAttribute : Attribute + { + public Type[] ArrayOfTypes { get; set; } + + public int[] ArrayOfInts { get; set; } + + public DayOfWeek[] Days { get; set; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/AttributesWithArrayConstructorArgumentsAttribute.cs b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/AttributesWithArrayConstructorArgumentsAttribute.cs new file mode 100644 index 0000000000..0e8db4bb30 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/AttributesWithArrayConstructorArgumentsAttribute.cs @@ -0,0 +1,36 @@ +// 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; + +namespace Microsoft.AspNet.Mvc.Razor +{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class AttributesWithArrayConstructorArgumentsAttribute : Attribute + { + public AttributesWithArrayConstructorArgumentsAttribute(string[] stringArgs, int[] intArgs) + { + StringArgs = stringArgs; + IntArgs = intArgs; + } + + public AttributesWithArrayConstructorArgumentsAttribute(string[] stringArgs, Type[] typeArgs, int[] intArgs) + { + StringArgs = stringArgs; + TypeArgs = typeArgs; + IntArgs = intArgs; + } + + public AttributesWithArrayConstructorArgumentsAttribute(int[] intArgs, Type[] typeArgs) + { + IntArgs = intArgs; + TypeArgs = typeArgs; + } + + public string[] StringArgs { get; } + + public Type[] TypeArgs { get; } + + public int[] IntArgs { get; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/BaseAttribute.cs b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/BaseAttribute.cs new file mode 100644 index 0000000000..870d216d2c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/BaseAttribute.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class BaseAttribute : Attribute + { + public string BaseProperty { get; set; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/DerivedAttribute.cs b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/DerivedAttribute.cs new file mode 100644 index 0000000000..c05c0ef95e --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/DerivedAttribute.cs @@ -0,0 +1,10 @@ +// 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.AspNet.Mvc.Razor +{ + public class DerivedAttribute : BaseAttribute + { + public string DerivedProperty { get; set; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/Microsoft.AspNet.Mvc.Razor.Precompilation.Files.xproj b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/Microsoft.AspNet.Mvc.Razor.Precompilation.Files.xproj new file mode 100644 index 0000000000..d8ba4d54ed --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/Microsoft.AspNet.Mvc.Razor.Precompilation.Files.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 83779c99-f281-46c3-afcc-d92d43a7c484 + Microsoft.AspNet.Mvc.Razor.Precompilation.Files + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/project.json b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/project.json new file mode 100644 index 0000000000..dea6060fd5 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Precompilation.Files/project.json @@ -0,0 +1,11 @@ +{ + "version": "1.0.0-*", + "compile": "../Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/*.cs", + "dependencies": { + "Microsoft.AspNet.Mvc.Razor": "6.0.0-*" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs index fa3d74895d..39be588b31 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs @@ -1,12 +1,10 @@ // 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. +#if MOQ using System; using System.Collections.Generic; using System.IO; -using System.Reflection; -using Microsoft.AspNet.Mvc.Razor.Precompilation; -using Microsoft.Dnx.Runtime; using Moq; using Xunit; @@ -288,4 +286,5 @@ private CompilationResult ThrowsIfCalled(RelativeFileInfo file) throw new Exception("Shouldn't be called"); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/PrecompiledViewsCompilerCacheProviderTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/PrecompiledViewsCompilerCacheProviderTest.cs index 6acc48a60a..be67365fdd 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/PrecompiledViewsCompilerCacheProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/PrecompiledViewsCompilerCacheProviderTest.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. +#if MOQ using System; using System.Reflection; using Microsoft.AspNet.Mvc.Razor.Precompilation; @@ -85,7 +86,6 @@ public void GetPrecompiledViews_ReturnsTypesSpecifiedByRazorFileInfoCollections( Assert.Equal(typeof(TestView2), type); } - private abstract class AbstractRazorFileInfoCollection : RazorFileInfoCollection { } @@ -140,3 +140,4 @@ private class TestView2 } } } +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.cs index c718617958..8a9aee40b1 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.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. +#if MOQ using System.IO; using System.Linq; using Microsoft.AspNet.FileProviders; @@ -225,3 +226,4 @@ private static IOptions GetOptions(IFileProvider filePro } } } +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.cs index d6f8d10c83..d59f399618 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.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. +#if MOQ using System; using System.IO; using System.Reflection; @@ -422,4 +423,5 @@ private static IOptions GetOptions(IFileProvider filePro return options.Object; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultTagHelperActivatorTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultTagHelperActivatorTest.cs index aff0ba923f..ee80198b2c 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultTagHelperActivatorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultTagHelperActivatorTest.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. +#if MOQ using System; using System.IO; using Microsoft.AspNet.Http; @@ -180,3 +181,4 @@ private class AnotherTestTagHelper : TagHelper } } } +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/BadFiles/TypeWithMalformedAttribute.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/BadFiles/TypeWithMalformedAttribute.cs new file mode 100644 index 0000000000..7f3fee5020 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/BadFiles/TypeWithMalformedAttribute.cs @@ -0,0 +1,18 @@ +// 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.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + [EditorBrowsable(Never)] + [CustomValidation(typeof(TypeDoesNotExist)] + [TargetElement("img" + public class TypeWithMalformedAttribute + { + + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/BadFiles/TypeWithMalformedProperties.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/BadFiles/TypeWithMalformedProperties.cs new file mode 100644 index 0000000000..232f7b0437 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/BadFiles/TypeWithMalformedProperties.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + public class TypeWithMalformedProperties + { + public DateTime DateTime { get } + + public int DateTime2 => "Hello world"; + + public string CustomOrder { get; set; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/BadFiles/TypeWithMissingReferences.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/BadFiles/TypeWithMissingReferences.cs new file mode 100644 index 0000000000..9e03554a17 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/BadFiles/TypeWithMissingReferences.cs @@ -0,0 +1,10 @@ +// 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.AspNet.Mvc.Razor.Precompilation +{ + public class TypeWithMissingReferences : ITagHelper + { + + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/AssemblyWithNonPublicTypes.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/AssemblyWithNonPublicTypes.cs new file mode 100644 index 0000000000..ff6a43e947 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/AssemblyWithNonPublicTypes.cs @@ -0,0 +1,28 @@ +// 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.AspNet.Mvc.Razor.Precompilation +{ + internal class InternalType + { + } + + public class PublicType + { + private class NestedPrivateType + { + } + } + + public class ContainerType + { + public class NestedType + { + + } + } + + public class Sum + { + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/AssemblyWithNonTypes.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/AssemblyWithNonTypes.cs new file mode 100644 index 0000000000..71b6ddc84d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/AssemblyWithNonTypes.cs @@ -0,0 +1,33 @@ +// 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.AspNet.Mvc.Razor.Precompilation +{ + public struct MyStruct + { + } + + public enum MyEnum + { + } + + public interface MyInterface + { + } + + public abstract class MyAbstractClass + { + } + + public partial class MyPartialClass + { + } + + public partial class MyPartialClass + { + } + + public sealed class MySealedClass + { + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/BaseTagHelper.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/BaseTagHelper.cs new file mode 100644 index 0000000000..5374865ec1 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/BaseTagHelper.cs @@ -0,0 +1,20 @@ +// 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.AspNet.Razor.Runtime.TagHelpers; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + public class BaseTagHelper : TagHelper + { + [Derived(DerivedProperty = "DerivedPropertyValue")] + public string BaseProperty { get; set; } + + [HtmlAttributeNotBound] + public virtual string VirtualProperty { get; set; } + + public int NewProperty { get; set; } + + public virtual new string Order { get; set; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/DerivedTagHelper.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/DerivedTagHelper.cs new file mode 100644 index 0000000000..e4e7903e03 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/DerivedTagHelper.cs @@ -0,0 +1,21 @@ +// 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.AspNet.Razor.Runtime.TagHelpers; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + public class DerivedTagHelper : BaseTagHelper + { + public override string VirtualProperty { get; set; } + + [Base(BaseProperty = "BaseValue")] + public string DerivedProperty { get; set; } + + [HtmlAttributeName("new-property")] + public new Type NewProperty { get; set; } + + public override string Order { get; set; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeDerivingFromITagHelper.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeDerivingFromITagHelper.cs new file mode 100644 index 0000000000..e820f759e6 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeDerivingFromITagHelper.cs @@ -0,0 +1,19 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + public class TypeDerivingFromITagHelper : ITagHelper + { + public int Order { get; } = 0; + + public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeDerivingFromTagHelper.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeDerivingFromTagHelper.cs new file mode 100644 index 0000000000..ac4cd331e3 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeDerivingFromTagHelper.cs @@ -0,0 +1,11 @@ +// 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.AspNet.Razor.Runtime.TagHelpers; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + public class TypeDerivingFromTagHelper : TagHelper + { + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeInGlobalNamespace.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeInGlobalNamespace.cs new file mode 100644 index 0000000000..64dfd1a902 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeInGlobalNamespace.cs @@ -0,0 +1,16 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; + +public class TypeInGlobalNamespace : ITagHelper +{ + public int Order { get; } = 0; + + public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + throw new NotImplementedException(); + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeWithAttributes.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeWithAttributes.cs new file mode 100644 index 0000000000..ae0aea7d17 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeWithAttributes.cs @@ -0,0 +1,57 @@ +// 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.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Resources; +using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + [TargetElement("img", Attributes = AppendVersionAttributeName + "," + SrcAttributeName)] + [TargetElement("image", Attributes = SrcAttributeName)] + [EditorBrowsable(EditorBrowsableState.Never)] + [ResponseCache(Location = ResponseCacheLocation.Client)] + [CustomValidation(typeof(Validator), "ValidationMethod", ErrorMessageResourceType = typeof(ResourceManager))] + [RestrictChildren("ol", "ul", "li", "dl", "dd")] + [Bind("include1", "include2", Prefix = "include-prefix")] + [AttributeWithArrayProperties( + ArrayOfInts = new[] { 7, 8, 9 }, + ArrayOfTypes = new[] { typeof(ITagHelper), typeof(Guid) }, + Days = new[] { DayOfWeek.Saturday })] + public class TypeWithAttributes + { + private const string AppendVersionAttributeName = "asp-append-version"; + private const string SrcAttributeName = "src"; + + [HtmlAttributeName(SrcAttributeName)] + [Required(AllowEmptyStrings = true)] + public string Src { get; set; } + + [HtmlAttributeName(AppendVersionAttributeName, DictionaryAttributePrefix = "prefix")] + [HtmlAttributeNotBound] + public bool AppendVersion { get; set; } + + [ViewContext] + [EditorBrowsable(EditorBrowsableState.Advanced)] + [AttributeWithArrayProperties( + ArrayOfInts = new int[0], + ArrayOfTypes = new[] { typeof(TypeWithAttributes) }, + Days = new[] { DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Wednesday, DayOfWeek.Sunday })] + public ViewContext ViewContext { get; set; } + + [AttributesWithArrayConstructorArguments(new[] { 1, 2 }, new[] { typeof(Uri), typeof(IList<>) })] + [AttributesWithArrayConstructorArguments(new[] { "Hello", "world" }, new[] { typeof(List) }, new int[0])] + [AttributesWithArrayConstructorArguments( + new[] { "world", "Hello" }, + new[] { typeof(IDictionary) }, + new[] { 1 })] + protected IHostingEnvironment HostingEnvironment { get; } + + [Derived(BaseProperty = "BaseValue", DerivedProperty = "DerivedValue")] + public string FormId { get; set; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeWithComplexPropertyFullNames.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeWithComplexPropertyFullNames.cs new file mode 100644 index 0000000000..dd059a01a4 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeWithComplexPropertyFullNames.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + public class TypeWithComplexPropertyFullNames + { + public int Property1 { get; set; } + + public int[] Property2 { get; set; } + + public List Property3 { get; set; } + + public List> Property4 { get; } + + public IReadOnlyDictionary, IEnumerable>> Property5 { get; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeWithDictionaryProperties.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeWithDictionaryProperties.cs new file mode 100644 index 0000000000..8bfa1c6ea0 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/Files/TypeWithDictionaryProperties.cs @@ -0,0 +1,34 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + public class TypeWithDictionaryProperties + { + public IDictionary RouteValues1 { get; set; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + public Dictionary RouteValues2 { get; set; } = + new Dictionary(); + + public IReadOnlyDictionary, float> RouteValues3 { get; set; } = + new Dictionary, float>(); + + public IDictionary CustomDictionary { get; set; } = + new Dictionary(); + + public IDictionary NonGenericDictionary { get; set; } = + new Dictionary(); + + public object ObjectType { get; set; } = + new Dictionary(); + } + + public class CustomType + { + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/PrecompilationTagHelperTypeResolverTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/PrecompilationTagHelperTypeResolverTest.cs new file mode 100644 index 0000000000..995c4d009d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Precompilation/PrecompilationTagHelperTypeResolverTest.cs @@ -0,0 +1,521 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.Mvc.Razor.Compilation; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Dnx.Compilation; +using Microsoft.Dnx.Runtime.Infrastructure; +using Microsoft.Framework.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor.Precompilation +{ + public class PrecompilationTagHelperTypeResolverTest + { + private static readonly Assembly ExecutingAssembly = typeof(PrecompilationTagHelperTypeResolverTest).GetTypeInfo().Assembly; + private static readonly Assembly RuntimeFilesAssembly = typeof(TypeDerivingFromTagHelper).GetTypeInfo().Assembly; + private static readonly TypeInfo TagHelperTypeInfo = typeof(ITagHelper).GetTypeInfo(); + private static readonly string GeneratedFilesAssemblyName = Path.GetRandomFileName() + "." + Path.GetRandomFileName(); + + [Theory] + [InlineData(typeof(TypeDerivingFromITagHelper))] + [InlineData(typeof(TypeDerivingFromTagHelper))] + [InlineData(typeof(TypeInGlobalNamespace))] + public void TypesReturnedFromGetTopLevelExportedTypes_ReturnTrueForImplmentsInterfaceTest(Type expected) + { + // Arrange + var compilation = GetCompilation(expected.Name); + var tagHelperResolver = new PrecompilationTagHelperTypeResolver(compilation); + + // Act + var exportedTypes = tagHelperResolver.GetExportedTypes(compilation.AssemblyName); + + // Assert + var actual = Assert.Single(exportedTypes); + AssertEqual(expected, actual); + } + + [Fact] + public void GetTopLevelExportedTypes_DoesNotReturnNonPublicOrNestedTypes() + { + // Arrange + var compilation = GetCompilation("AssemblyWithNonPublicTypes"); + var tagHelperResolver = new PrecompilationTagHelperTypeResolver(compilation); + + // Act + var exportedTypes = tagHelperResolver.GetExportedTypes(compilation.AssemblyName); + + // Assert + Assert.Collection(exportedTypes, + typeInfo => + { + AssertEqual(typeof(PublicType), typeInfo); + }, + typeInfo => + { + AssertEqual(typeof(ContainerType), typeInfo); + }, + typeInfo => + { + AssertEqual(typeof(Sum<>), typeInfo); + }); + } + + [Fact] + public void GetTopLevelExportedTypes_DoesNotReturnValueTypesOrInterfaces() + { + // Arrange + var compilation = GetCompilation("AssemblyWithNonTypes"); + var tagHelperResolver = new PrecompilationTagHelperTypeResolver(compilation); + + // Act + var exportedTypes = tagHelperResolver.GetExportedTypes(compilation.AssemblyName); + + // Assert + Assert.Collection(exportedTypes, + typeInfo => + { + AssertEqual(typeof(MyAbstractClass), typeInfo); + }, + typeInfo => + { + AssertEqual(typeof(MyPartialClass), typeInfo); + }, + typeInfo => + { + AssertEqual(typeof(MySealedClass), typeInfo); + }); + } + + [Fact] + public void GetExportedTypes_PopulatesAttributes() + { + // Arrange + var expected = typeof(TypeWithAttributes).GetTypeInfo(); + var compilation = GetCompilation(expected.Name); + var tagHelperResolver = new PrecompilationTagHelperTypeResolver(compilation); + + // Act + var exportedTypes = tagHelperResolver.GetExportedTypes(compilation.AssemblyName); + + // Assert + var actual = Assert.Single(exportedTypes); + AssertEqual(expected.AsType(), actual); + + AssertAttributes( + expected, + actual, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.Tag, actualAttribute.Tag); + Assert.Equal(expectedAttribute.Attributes, actualAttribute.Attributes); + }); + + // Verify if enum in attribute constructors works. + AssertAttributes( + expected, + actual, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.State, actualAttribute.State); + }); + + // Verify if enum in attribute property works. + AssertAttributes( + expected, + actual, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.Location, actualAttribute.Location); + }); + + // Verify if Type in attribute constructor and property works. + AssertAttributes( + expected, + actual, + (expectedAttribute, actualAttribute) => + { + Assert.Same(expectedAttribute.ValidatorType, actualAttribute.ValidatorType); + Assert.Equal(expectedAttribute.Method, actualAttribute.Method); + Assert.Same( + expectedAttribute.ErrorMessageResourceType, + actualAttribute.ErrorMessageResourceType); + }); + + // Verify if array arguments work in constructor. + AssertAttributes( + expected, + actual, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.Include, actualAttribute.Include); + Assert.Equal(expectedAttribute.Prefix, actualAttribute.Prefix); + Assert.Equal(expectedAttribute.PredicateProviderType, actualAttribute.PredicateProviderType); + }); + + // Verify if array arguments work in property. + AssertAttributes( + expected, + actual, + (expectedAttribute, actualAttribute) => + { + Assert.Equal( + expectedAttribute.ChildTagNames, + actualAttribute.ChildTagNames); + }); + + // Complex array bindings + AssertAttributes( + expected, + actual, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.ArrayOfTypes, actualAttribute.ArrayOfTypes); + Assert.Equal(expectedAttribute.ArrayOfInts, actualAttribute.ArrayOfInts); + Assert.Equal(expectedAttribute.Days, actualAttribute.Days); + }); + + var expectedProperties = expected.DeclaredProperties; + Assert.Collection(actual.Properties, + property => + { + Assert.Equal(nameof(TypeWithAttributes.Src), property.Name); + var expectedProperty = Assert.Single(expectedProperties, p => p.Name == property.Name); + AssertAttributes( + expectedProperty, + property, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.Name, actualAttribute.Name); + Assert.Equal(expectedAttribute.DictionaryAttributePrefix, actualAttribute.DictionaryAttributePrefix); + }); + + // Verify boolean values bind. + AssertAttributes( + expectedProperty, + property, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.AllowEmptyStrings, actualAttribute.AllowEmptyStrings); + }); + }, + property => + { + Assert.Equal(nameof(TypeWithAttributes.AppendVersion), property.Name); + var expectedProperty = Assert.Single(expectedProperties, p => p.Name == property.Name); + AssertAttributes( + expectedProperty, + property, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.Name, actualAttribute.Name); + Assert.Equal(expectedAttribute.DictionaryAttributePrefix, actualAttribute.DictionaryAttributePrefix); + }); + + // Attribute without constructor arguments or properties. + Assert.Single(expectedProperty.GetCustomAttributes()); + Assert.Single(property.GetCustomAttributes()); + }, + property => + { + Assert.Equal(nameof(TypeWithAttributes.ViewContext), property.Name); + var expectedProperty = Assert.Single(expectedProperties, p => p.Name == property.Name); + + AssertAttributes( + expectedProperty, + property, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.State, actualAttribute.State); + }); + + // Complex array bindings in properties + AssertAttributes( + expected, + actual, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.ArrayOfTypes, actualAttribute.ArrayOfTypes); + Assert.Equal(expectedAttribute.ArrayOfInts, actualAttribute.ArrayOfInts); + Assert.Equal(expectedAttribute.Days, actualAttribute.Days); + }); + }, + property => + { + Assert.Equal("HostingEnvironment", property.Name); + Assert.Single(expectedProperties, p => p.Name == property.Name); + + // Complex array bindings in constructor arguments + AssertAttributes( + expected, + actual, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.StringArgs, actualAttribute.StringArgs); + Assert.Equal(expectedAttribute.IntArgs, actualAttribute.IntArgs); + Assert.Equal(expectedAttribute.TypeArgs, actualAttribute.TypeArgs); + }); + }, + property => + { + Assert.Equal(nameof(TypeWithAttributes.FormId), property.Name); + var expectedProperty = Assert.Single(expectedProperties, p => p.Name == property.Name); + + AssertAttributes( + expectedProperty, + property, + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.BaseProperty, actualAttribute.BaseProperty); + Assert.Equal(expectedAttribute.DerivedProperty, actualAttribute.DerivedProperty); + }); + }); + } + + [Fact] + public void GetExportedTypes_WithDerivedAttributes() + { + // Arrange + var expected = typeof(DerivedTagHelper); + var compilation = GetCompilation(expected.Name); + var tagHelperResolver = new PrecompilationTagHelperTypeResolver(compilation); + + // Act + var exportedTypes = tagHelperResolver.GetExportedTypes(compilation.AssemblyName); + + // Assert + var actual = Assert.Single(exportedTypes); + AssertEqual(expected, actual); + + var expectedProperties = expected.GetProperties(); + AssertAttributes( + expectedProperties.First(p => p.Name == nameof(DerivedTagHelper.DerivedProperty)), + actual.Properties.First(p => p.Name == nameof(DerivedTagHelper.DerivedProperty)), + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.BaseProperty, actualAttribute.BaseProperty); + }); + + AssertAttributes( + expectedProperties.First(p => p.Name == nameof(DerivedTagHelper.NewProperty) && + p.PropertyType == typeof(Type)), + actual.Properties.First(p => p.Name == nameof(DerivedTagHelper.NewProperty) && + p.PropertyType.Name == typeof(Type).Name), + (expectedAttribute, actualAttribute) => + { + Assert.Equal(expectedAttribute.Name, actualAttribute.Name); + Assert.Equal( + expectedAttribute.DictionaryAttributePrefix, + actualAttribute.DictionaryAttributePrefix); + }); + + var expectedVirtualProperty = expectedProperties.First( + p => p.Name == nameof(DerivedTagHelper.VirtualProperty)); + var actualVirtualProperty = actual.Properties.First( + p => p.Name == nameof(DerivedTagHelper.VirtualProperty)); + + Assert.Empty(expectedVirtualProperty.GetCustomAttributes()); + Assert.Empty(actualVirtualProperty.GetCustomAttributes()); + } + + [Fact] + public void GetExportedTypes_CorrectlyIdentifiesIfTypeDerivesFromDictionary() + { + // Arrange + var compilation = GetCompilation(nameof(TypeWithDictionaryProperties)); + var tagHelperResolver = new PrecompilationTagHelperTypeResolver(compilation); + + // Act + var exportedTypes = tagHelperResolver.GetExportedTypes(compilation.AssemblyName); + + // Assert + Assert.Collection(exportedTypes, + typeInfo => + { + AssertEqual(typeof(TypeWithDictionaryProperties), typeInfo); + }, + typeInfo => + { + AssertEqual(typeof(CustomType), typeInfo); + }); + } + + [Fact] + public void GetExportedTypes_WorksIfAttributesAreMalformedErrors() + { + // Arrange + var compilation = GetCompilation("TypeWithMalformedAttribute", "BadFiles"); + var tagHelperResolver = new PrecompilationTagHelperTypeResolver(compilation); + + // Act + var exportedTypes = tagHelperResolver.GetExportedTypes(compilation.AssemblyName); + + // Assert + var actual = Assert.Single(exportedTypes); + var targetElementAttribute = Assert.Single(actual.GetCustomAttributes()); + Assert.Equal("img", targetElementAttribute.Tag); + Assert.Null(targetElementAttribute.Attributes); + + var editorBrowsableAttribute = Assert.Single(actual.GetCustomAttributes()); + Assert.Equal(default(EditorBrowsableState), editorBrowsableAttribute.State); + + Assert.Throws(() => actual.GetCustomAttributes()); + } + + [Fact] + public void GetExportedTypes_WorksForPropertiesWithErrors() + { + // Arrange + var compilation = GetCompilation("TypeWithMalformedProperties", "BadFiles"); + var tagHelperResolver = new PrecompilationTagHelperTypeResolver(compilation); + + // Act + var exportedTypes = tagHelperResolver.GetExportedTypes(compilation.AssemblyName); + + // Assert + var actual = Assert.Single(exportedTypes); + Assert.Collection(actual.Properties, + property => + { + Assert.Equal("DateTime", property.Name); + Assert.Equal(typeof(DateTime).Name, property.PropertyType.Name); + Assert.True(property.HasPublicGetter); + Assert.False(property.HasPublicSetter); + }, + property => + { + Assert.Equal("DateTime2", property.Name); + Assert.Equal(typeof(int).Name, property.PropertyType.Name); + Assert.True(property.HasPublicGetter); + Assert.False(property.HasPublicSetter); + }, + property => + { + Assert.Equal("CustomOrder", property.Name); + Assert.Equal(typeof(string).Name, property.PropertyType.Name); + Assert.True(property.HasPublicGetter); + Assert.True(property.HasPublicSetter); + }); + } + + [Fact] + public void GetExportedTypes_WorksForTypesWithErrors() + { + // Arrange + var compilation = GetCompilation("TypeWithMissingReferences", "BadFiles"); + var tagHelperResolver = new PrecompilationTagHelperTypeResolver(compilation); + + // Act + var exportedTypes = tagHelperResolver.GetExportedTypes(compilation.AssemblyName); + + // Assert + var type = Assert.Single(exportedTypes); + Assert.False(type.ImplementsInterface(TagHelperTypeInfo)); + } + + private static void AssertAttributes( + MemberInfo expected, + IMemberInfo actual, + Action assertItem) + where TAttribute : Attribute + { + Assert.Equal( + expected.GetCustomAttributes(), + actual.GetCustomAttributes(), + new DelegateAssertion(assertItem)); + } + + private static void AssertEqual(Type expected, ITypeInfo actual) + { + AssertEqual(new RuntimeTypeInfo(expected.GetTypeInfo()), actual, assertProperties: true); + } + + private static void AssertEqual(ITypeInfo expected, ITypeInfo actual, bool assertProperties) + { + var actualFullName = actual.FullName.Replace( + GeneratedFilesAssemblyName, + RuntimeFilesAssembly.GetName().Name); + + Assert.Equal(expected.Name, actual.Name); + Assert.Equal(expected.FullName, actualFullName); + Assert.Equal(expected.IsPublic, actual.IsPublic); + Assert.Equal(expected.IsAbstract, actual.IsAbstract); + Assert.Equal(expected.IsGenericType, actual.IsGenericType); + Assert.Equal( + expected.ImplementsInterface(TagHelperTypeInfo), + actual.ImplementsInterface(TagHelperTypeInfo)); + Assert.Equal( + expected.GetGenericDictionaryParameterNames(), + actual.GetGenericDictionaryParameterNames()); + + if (assertProperties) + { + Assert.Equal( + expected.Properties.OrderBy(p => p.Name), + actual.Properties.OrderBy(p => p.Name), + new DelegateAssertion((x, y) => AssertEqual(x, y))); + } + } + + private static void AssertEqual(IPropertyInfo expected, IPropertyInfo actual) + { + Assert.Equal(expected.Name, actual.Name); + Assert.Equal(expected.HasPublicGetter, actual.HasPublicGetter); + Assert.Equal(expected.HasPublicSetter, actual.HasPublicSetter); + AssertEqual(expected.PropertyType, actual.PropertyType, assertProperties: false); + } + + private CodeAnalysis.Compilation GetCompilation(string fileName, string directory = "Files") + { + var resourceText = ResourceFile.ReadResource( + ExecutingAssembly, + $"Precompilation.{directory}.{fileName}.cs", + sourceFile: true); + var assemblyVersion = RuntimeFilesAssembly.GetName().Version; + + var syntaxTrees = new[] + { + CSharpSyntaxTree.ParseText(resourceText), + CSharpSyntaxTree.ParseText( + $"[assembly: {typeof(AssemblyVersionAttribute).FullName}(\"{assemblyVersion}\")]") + }; + + var references = RoslynCompilationService.GetApplicationReferences( + new ConcurrentDictionary(StringComparer.Ordinal), + CallContextServiceLocator.Locator.ServiceProvider.GetService(), + ExecutingAssembly.GetName().Name); + + return CSharpCompilation.Create( + GeneratedFilesAssemblyName, + syntaxTrees, + references); + } + + private class DelegateAssertion : IEqualityComparer + { + private readonly Action _assert; + + public DelegateAssertion(Action assert) + { + _assert = assert; + } + + public bool Equals(T x, T y) + { + _assert(x, y); + return true; + } + + public int GetHashCode(T obj) => 0; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageActivatorTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageActivatorTest.cs index 05587342dc..c9179a0274 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageActivatorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageActivatorTest.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. +#if MOQ using System; using System.Globalization; using System.IO; @@ -260,4 +261,5 @@ private class MyModel { } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs index f12a7306b4..6c2ae2ef3e 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.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. +#if MOQ using System; using System.IO; using System.Linq.Expressions; @@ -163,4 +164,5 @@ public class RazorPageCreateModelExpressionSubSubModel public string Name { get; set; } } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateTagHelperTest.cs index a2861cc661..3e51546a93 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateTagHelperTest.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. +#if MOQ using System; using System.Collections.Generic; using System.IO; @@ -128,4 +129,5 @@ private class MyService { } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs index 4d996fdc83..6ab387ec51 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.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. +#if MOQ using System; using System.Collections.Generic; using System.IO; @@ -1856,4 +1857,5 @@ public HelperResult RenderBodyPublic() } } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs index f1ac9f206f..6068fe99c8 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.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. +#if MOQ using System; using System.Collections.Generic; using System.IO; @@ -341,4 +342,5 @@ public override string ToString() } } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs index 488302994d..e9ba86d72b 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.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. +#if MOQ using System.Collections.Generic; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Mvc.Rendering; @@ -1136,3 +1137,4 @@ public override IEnumerable AreaViewLocationFormats } } } +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewFactoryTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewFactoryTest.cs index da33559761..83bcb192c5 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewFactoryTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewFactoryTest.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. +#if MOQ using Moq; using Xunit; @@ -49,4 +50,5 @@ public void GetView_SetsRazorPage() Assert.Same(razorView.RazorPage, page); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 713cd3eef8..bd0942e221 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.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. +#if MOQ using System; using System.Collections.Generic; using System.IO; @@ -1374,4 +1375,5 @@ public override Task ExecuteAsync() } } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/TagHelpers/UrlResolutionTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/TagHelpers/UrlResolutionTagHelperTest.cs index 346db8856e..0ff929158f 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/TagHelpers/UrlResolutionTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/TagHelpers/UrlResolutionTagHelperTest.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. +#if MOQ using System; using System.Reflection; using Microsoft.AspNet.Mvc.Rendering; @@ -168,3 +169,4 @@ public void Process_ThrowsWhenEncodingNeededAndIUrlHelperActsUnexpectedly() } } } +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/project.json b/test/Microsoft.AspNet.Mvc.Razor.Test/project.json index 10d0951d6f..75d0738eb8 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/project.json +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/project.json @@ -4,11 +4,16 @@ "../Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileInfo.cs", "../Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileTrigger.cs" ], + "resource": [ + "Precompilation/Files/*.cs", + "Precompilation/BadFiles/*.cs" + ], "dependencies": { "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.Mvc.DataAnnotations": "6.0.0-*", "Microsoft.AspNet.Mvc.Formatters.Xml": "6.0.0-*", "Microsoft.AspNet.Mvc.Razor": "6.0.0-*", + "Microsoft.AspNet.Mvc.Razor.Precompilation.Files": "1.0.0-*", "Microsoft.AspNet.Mvc.TestCommon": { "version": "6.0.0-*", "type": "build" @@ -26,7 +31,12 @@ "dnx451": { "dependencies": { "Moq": "4.2.1312.1622" + }, + "compilationOptions": { + "define": [ "MOQ" ] } + }, + "dnxcore50": { } }, "exclude": [