Skip to content

Commit

Permalink
Do build time discovery of MVC ApplicationParts (#598)
Browse files Browse the repository at this point in the history
* Do build time discovery of MVC ApplicationParts
  • Loading branch information
pranavkm authored May 17, 2019
1 parent 17af3ef commit e79d560
Show file tree
Hide file tree
Showing 15 changed files with 659 additions and 7 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</PropertyGroup>

<PropertyGroup Label="Build Settings">
<LangVersion>7.3</LangVersion>
<LangVersion>8.0</LangVersion>
<StrongNameKeyId>MicrosoftAspNetCore</StrongNameKeyId>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<DebugType>portable</DebugType>
Expand Down
4 changes: 4 additions & 0 deletions eng/Version.Details.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
<Uri>https://github.com/dotnet/corefx</Uri>
<Sha>a28176b5ec68b6da1472934fe9493790d1665cae</Sha>
</Dependency>
<Dependency Name="System.Reflection.Metadata" Version="1.7.0-preview6.19264.9" CoherentParentDependency="Microsoft.NETCore.App">
<Uri>https://github.com/dotnet/corefx</Uri>
<Sha>a28176b5ec68b6da1472934fe9493790d1665cae</Sha>
</Dependency>
<Dependency Name="System.Text.Encodings.Web" Version="4.6.0-preview6.19264.9" CoherentParentDependency="Microsoft.NETCore.App">
<Uri>https://github.com/dotnet/corefx</Uri>
<Sha>a28176b5ec68b6da1472934fe9493790d1665cae</Sha>
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<MicrosoftNETCorePlatformsPackageVersion>3.0.0-preview6.19264.9</MicrosoftNETCorePlatformsPackageVersion>
<SystemDiagnosticsDiagnosticSourcePackageVersion>4.6.0-preview6.19264.9</SystemDiagnosticsDiagnosticSourcePackageVersion>
<SystemTextEncodingsWebPackageVersion>4.6.0-preview6.19264.9</SystemTextEncodingsWebPackageVersion>
<SystemReflectionMetadataPackageVersion>1.7.0-preview6.19264.9</SystemReflectionMetadataPackageVersion>
</PropertyGroup>
<!--
Expand Down
14 changes: 14 additions & 0 deletions src/Razor/src/Microsoft.NET.Sdk.Razor/AssemblyItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.AspNetCore.Razor.Tasks
{
public class AssemblyItem
{
public string Path { get; set; }

public bool IsSystemReference { get; set; }

public string AssemblyName { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Razor is a markup syntax for adding server-side logic to web pages. This package contains MSBuild support for Razor.</Description>
<TargetFrameworks>netcoreapp3.0;netstandard2.0;net46</TargetFrameworks>
<TargetFrameworks>netcoreapp3.0;net46</TargetFrameworks>

<TargetName>Microsoft.NET.Sdk.Razor.Tasks</TargetName>
<NuspecFile>$(MSBuildProjectName).nuspec</NuspecFile>
Expand All @@ -17,8 +17,9 @@
<PackageReference Include="Microsoft.Build.Framework" Version="$(MicrosoftBuildFrameworkPackageVersion)" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="$(MicrosoftBuildUtilitiesCorePackageVersion)" />
<PackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" Version="$(MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion)" />
<PackageReference Include="System.Reflection.Metadata" Version="$(SystemReflectionMetadataPackageVersion)" Condition="'$(TargetFramework)'=='net46'" />

<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.Razor.Extensions\Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj" ReferenceOutputAssembly="false" Condition="'$(TargetFramework)'=='netstandard2.0'" />
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.Razor.Extensions\Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj" ReferenceOutputAssembly="false" Condition="'$(TargetFramework)'=='netcoreapp3.0'" />
<ProjectReference Include="..\Microsoft.AspNetCore.Razor.Tools\Microsoft.AspNetCore.Razor.Tools.csproj" ReferenceOutputAssembly="false" Condition="'$(TargetFramework)'=='netcoreapp3.0'" />
</ItemGroup>

Expand Down Expand Up @@ -54,7 +55,10 @@

<ItemGroup>
<ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\net46*\Microsoft.NET.Sdk.Razor.Tasks.*" />
<ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\netstandard2.0*\Microsoft.NET.Sdk.Razor.Tasks.*" />
<ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\netcoreapp3.0*\Microsoft.NET.Sdk.Razor.Tasks.*" />

<ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\net46*\System.Collections.Immutable.dll" />
<ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\net46*\System.Reflection.Metadata.dll" />
</ItemGroup>

<Copy SourceFiles="@(ProjectOutput)" DestinationFiles="$(SdkOutputPath)tasks\%(RecursiveDir)%(FileName)%(Extension)" SkipUnchangedFiles="true">
Expand Down
169 changes: 169 additions & 0 deletions src/Razor/src/Microsoft.NET.Sdk.Razor/ReferenceResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// 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.Reflection.Metadata;
using System.Reflection.PortableExecutable;

namespace Microsoft.AspNetCore.Razor.Tasks
{
/// <summary>
/// Resolves assemblies that reference one of the specified "targetAssemblies" either directly or transitively.
/// </summary>
public class ReferenceResolver
{
private readonly HashSet<string> _mvcAssemblies;
private readonly Dictionary<string, ClassifiedAssemblyItem> _lookup = new Dictionary<string, ClassifiedAssemblyItem>(StringComparer.Ordinal);

public ReferenceResolver(IReadOnlyList<string> targetAssemblies, IReadOnlyList<AssemblyItem> assemblyItems)
{
_mvcAssemblies = new HashSet<string>(targetAssemblies, StringComparer.Ordinal);

foreach (var item in assemblyItems)
{
var classifiedItem = new ClassifiedAssemblyItem(item);
_lookup[item.AssemblyName] = classifiedItem;
}
}

public IReadOnlyList<string> ResolveAssemblies()
{
var applicationParts = new List<string>();
foreach (var item in _lookup)
{
var classification = Resolve(item.Value);
if (classification == DependencyClassification.ReferencesMvc)
{
applicationParts.Add(item.Key);
}

// It's not interesting for us to know if a dependency has a classification of MvcReference.
// All applications targeting the Microsoft.AspNetCore.App will have have a reference to Mvc.
}

return applicationParts;
}

private DependencyClassification Resolve(ClassifiedAssemblyItem classifiedItem)
{
if (classifiedItem.DependencyClassification != DependencyClassification.Unknown)
{
return classifiedItem.DependencyClassification;
}

if (classifiedItem.AssemblyItem == null)
{
// We encountered a dependency that isn't part of this assembly's dependency set. We'll see if it happens to be an MVC assembly.
// This might be useful in scenarios where the app does not have a framework reference at the entry point,
// but the transitive dependency does.
classifiedItem.DependencyClassification = _mvcAssemblies.Contains(classifiedItem.Name) ?
DependencyClassification.MvcReference :
DependencyClassification.DoesNotReferenceMvc;

return classifiedItem.DependencyClassification;
}

if (classifiedItem.AssemblyItem.IsSystemReference)
{
// We do not allow transitive references to MVC via a framework reference to count.
// e.g. depending on Microsoft.AspNetCore.SomeThingNewThatDependsOnMvc would not result in an assembly being treated as
// referencing MVC.
classifiedItem.DependencyClassification = _mvcAssemblies.Contains(classifiedItem.Name) ?
DependencyClassification.MvcReference :
DependencyClassification.DoesNotReferenceMvc;

return classifiedItem.DependencyClassification;
}

if (_mvcAssemblies.Contains(classifiedItem.Name))
{
classifiedItem.DependencyClassification = DependencyClassification.MvcReference;
return classifiedItem.DependencyClassification;
}

var dependencyClassification = DependencyClassification.DoesNotReferenceMvc;
foreach (var assemblyItem in GetReferences(classifiedItem.AssemblyItem.Path))
{
var classification = Resolve(assemblyItem);
if (classification == DependencyClassification.MvcReference || classification == DependencyClassification.ReferencesMvc)
{
dependencyClassification = DependencyClassification.ReferencesMvc;
break;
}
}

classifiedItem.DependencyClassification = dependencyClassification;
return dependencyClassification;
}

protected virtual IReadOnlyList<ClassifiedAssemblyItem> GetReferences(string file)
{
try
{
using var peReader = new PEReader(File.OpenRead(file));
if (!peReader.HasMetadata)
{
return Array.Empty<ClassifiedAssemblyItem>(); // not a managed assembly
}

var metadataReader = peReader.GetMetadataReader();

var assemblyItems = new List<ClassifiedAssemblyItem>();
foreach (var handle in metadataReader.AssemblyReferences)
{
var reference = metadataReader.GetAssemblyReference(handle);
var referenceName = metadataReader.GetString(reference.Name);

if (_lookup.TryGetValue(referenceName, out var classifiedItem))
{
assemblyItems.Add(classifiedItem);
}
else
{
// A dependency references an item that isn't referenced by this project.
// We'll construct an item for so that we can calculate the classification based on it's name.
assemblyItems.Add(new ClassifiedAssemblyItem(referenceName));
}
}

return assemblyItems;
}
catch (BadImageFormatException)
{
// not a PE file, or invalid metadata
}

return Array.Empty<ClassifiedAssemblyItem>(); // not a managed assembly
}

protected enum DependencyClassification
{
Unknown,
DoesNotReferenceMvc,
ReferencesMvc,
MvcReference,
}

protected class ClassifiedAssemblyItem
{
public ClassifiedAssemblyItem(AssemblyItem classifiedItem)
: this(classifiedItem.AssemblyName)
{
AssemblyItem = classifiedItem;
}

public ClassifiedAssemblyItem(string name)
{
Name = name;
}

public string Name { get; }

public AssemblyItem AssemblyItem { get; }

public DependencyClassification DependencyClassification { get; set; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.AspNetCore.Razor.Tasks
{
public class FindAssembliesWithReferencesTo : Task
{
[Required]
public ITaskItem[] TargetAssemblyNames { get; set; }

[Required]
public ITaskItem[] Assemblies { get; set; }

[Output]
public string[] ResolvedAssemblies { get; set; }

public override bool Execute()
{
var referenceItems = new List<AssemblyItem>();
foreach (var item in Assemblies)
{
const string FusionNameKey = "FusionName";
var fusionName = item.GetMetadata(FusionNameKey);
if (string.IsNullOrEmpty(fusionName))
{
Log.LogError($"Missing required metadata '{FusionNameKey}' for '{item.ItemSpec}.");
return false;
}

var assemblyName = new AssemblyName(fusionName).Name;
referenceItems.Add(new AssemblyItem
{
AssemblyName = assemblyName,
IsSystemReference = item.GetMetadata("IsSystemReference") == "true",
Path = item.ItemSpec,
});
}

var targetAssemblyNames = TargetAssemblyNames.Select(s => s.ItemSpec).ToList();

var provider = new ReferenceResolver(targetAssemblyNames, referenceItems);
var assemblyNames = provider.ResolveAssemblies();

ResolvedAssemblies = assemblyNames.ToArray();

return !Log.HasLoggedErrors;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!--
***********************************************************************************************
Microsoft.NET.Sdk.Razor.ApplicationPartsDiscovery
WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
created a backup copy. Incorrect changes to this file will make it
impossible to load or build your projects from the command-line or the IDE.
Copyright (c) .NET Foundation. All rights reserved.
***********************************************************************************************
-->

<Project ToolsVersion="14.0">
<UsingTask
TaskName="Microsoft.AspNetCore.Razor.Tasks.FindAssembliesWithReferencesTo"
AssemblyFile="$(RazorSdkBuildTasksAssembly)"
Condition="'$(RazorSdkBuildTasksAssembly)' != ''" />

<PropertyGroup>
<GenerateMvcApplicationPartsAttribute Condition="'$(GenerateMvcApplicationPartsAssemblyAttributes)' == '' AND '$(OutputType)' == 'Exe'">true</GenerateMvcApplicationPartsAttribute>

<CoreCompileDependsOn Condition="'$(GenerateMvcApplicationPartsAttribute)' == 'true' AND '$(DesignTimeBuild)' != 'true'">
_DiscoverMvcApplicationParts;
$(CoreCompileDependsOn);
</CoreCompileDependsOn>

<_MvcApplicationPartAttributeGeneratedFile>$(IntermediateOutputPath)$(TargetName).MvcApplicationPartsAssemblyInfo$(DefaultLanguageSourceExtension)</_MvcApplicationPartAttributeGeneratedFile>
</PropertyGroup>

<Target
Name="_DiscoverMvcApplicationParts"
Inputs="$(ProjectAssetsFile);$(MSBuildAllProjects)"
Outputs="$(_MvcApplicationPartAttributeGeneratedFile)"
DependsOnTargets="ResolveAssemblyReferences">

<ItemGroup>
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Abstractions" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.ApiExplorer" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Core" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Cors" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.DataAnnotations" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Formatters.Json" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Formatters.Xml" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Localization" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Razor" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.RazorPages" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.TagHelpers" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
</ItemGroup>

<FindAssembliesWithReferencesTo Assemblies="@(ReferencePath)" TargetAssemblyNames="@(_MvcAssemblyName)">
<Output TaskParameter="ResolvedAssemblies" ItemName="_ApplicationPartAssemblyNames"/>
</FindAssembliesWithReferencesTo>

<ItemGroup>
<_MvcApplicationPartAttribute Include="Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartAttribute">
<_Parameter1>%(_ApplicationPartAssemblyNames.Identity)</_Parameter1>
</_MvcApplicationPartAttribute>
</ItemGroup>

<!-- If we found application part assemblies, generate attributes for it and add it to compilation list -->
<WriteCodeFragment
AssemblyAttributes="@(_MvcApplicationPartAttribute)"
Language="$(Language)"
OutputFile="$(_MvcApplicationPartAttributeGeneratedFile)"
Condition="'@(_ApplicationPartAssemblyNames->Count())' != '0'" />

<ItemGroup Condition="'@(_ApplicationPartAssemblyNames->Count())' != '0'">
<Compile Remove="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'!='F#'" />
<Compile Include="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'!='F#'" />

<CompileBefore Remove="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'=='F#'" />
<CompileBefore Include="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'=='F#'" />
</ItemGroup>

<!--
If we did not find any application parts, produce an empty file which is not added to compilation.
This is required to play nicely with incremental builds.
-->
<Touch
Files="$(_MvcApplicationPartAttributeGeneratedFile)"
AlwaysCreate="true" />

<ItemGroup>
<FileWrites Include="$(_MvcApplicationPartAttributeGeneratedFile)" />
</ItemGroup>
</Target>
</Project>
Loading

0 comments on commit e79d560

Please sign in to comment.