-
Notifications
You must be signed in to change notification settings - Fork 476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor ProjectReferences in AppHost #1726
Changes from 7 commits
4fc3496
dc6235b
935095c
8290c72
fdb8f60
9f8a853
3917b31
4fc8333
6a9d7fc
ab8c773
b2ef09a
f9eb383
a005c5a
4686cd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,5 @@ | ||
<Project> | ||
|
||
<!-- *** BEGIN *** --> | ||
|
||
<!-- | ||
A property-based workload cannot import props files (except AutoImport.props, which has very tight limitations. | ||
Instead, we copy everything from Aspire.Hosting.props into these targets. | ||
|
@@ -20,7 +18,21 @@ | |
<IsPackable Condition="'$(IsPackable)' == ''">false</IsPackable> | ||
</PropertyGroup> | ||
|
||
<!-- *** END *** --> | ||
<!-- | ||
Default AppHost ProjectReference items to not be referenced in the AppHost, unless it is AspireHostLibrary=true. | ||
This defaulting needs to happen in the SDK targets so this metadata affects NuGet Restore. | ||
--> | ||
<ItemGroup Condition="'$(IsAspireHost)' == 'true'"> | ||
|
||
<ProjectReference Update="@(ProjectReference)"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add a global opt-out property for this behavior? e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you think of a good reason why? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still slightly uneasy about taking over ProjectReferences and it's common for MSBuild defaults to have an opt-out. But we can wait for feedback. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the consensus here? I can add a property if people think it is valuable. (I also think we can add it later if we get feedback as well.) |
||
<_IsServiceApp Condition="'%(_IsServiceApp)' == '' and '%(AspireHostLibrary)' != 'true'">true</_IsServiceApp> | ||
|
||
<ReferenceOutputAssembly Condition="'%(ReferenceOutputAssembly)' == '' and '%(_IsServiceApp)' == 'true'">false</ReferenceOutputAssembly> | ||
<SkipGetTargetFrameworkProperties Condition="'%(SkipGetTargetFrameworkProperties)' == '' and '%(_IsServiceApp)' == 'true'">true</SkipGetTargetFrameworkProperties> | ||
joperezr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<ExcludeAssets Condition="'%(ExcludeAssets)' == '' and '%(_IsServiceApp)' == 'true'">all</ExcludeAssets> | ||
</ProjectReference> | ||
|
||
</ItemGroup> | ||
|
||
<Target Name="__WarnOnAspireCapabilityMissing" BeforeTargets="PrepareForBuild" Condition="!@(ProjectCapability->AnyHaveMetadataValue('Identity', 'Aspire'))"> | ||
<Warning Code="ASPIRE002" Text="$(MSBuildProjectName) is an Aspire AppHost project but necessary dependencies aren't present. Are you missing an Aspire.Hosting PackageReference?" /> | ||
|
@@ -37,6 +49,6 @@ | |
also contains Aspire.Hosting.Orchestration and vice versa. Once Aspire/DCP are public | ||
we can move it to WorkloadManifest.targets to ensure future correctness. | ||
--> | ||
<Import Project="Sdk.targets" Sdk="Aspire.Hosting.Orchestration" /> | ||
<Import Project="Sdk.targets" Sdk="Aspire.Hosting.Orchestration" Condition="'$(_SuppressImportAspireHostingOrchestrationTargets)' != 'true'" /> | ||
joperezr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,18 +12,22 @@ | |
<ProjectCapability Include="Aspire" Condition=" '$(IsAspireHost)' == 'true' " /> | ||
</ItemGroup> | ||
|
||
<Target Name="_CreateServiceAppProjects"> | ||
|
||
<ItemGroup> | ||
<_ServiceAppProject Include="@(ProjectReference->WithMetadataValue('_IsServiceApp', 'true'))" /> | ||
</ItemGroup> | ||
|
||
</Target> | ||
|
||
<!-- Generate the data structures for doing the codegen for service references --> | ||
<Target Name="CreateServiceMetadataSources" | ||
DependsOnTargets="FindReferenceAssembliesForReferences" | ||
Inputs="@(ReferencePathWithRefAssemblies)" | ||
Outputs="@(ReferencePathWithRefAssemblies)"> | ||
DependsOnTargets="_CreateServiceAppProjects"> | ||
eerhardt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<ItemGroup> | ||
<ServiceMetadataSource Include="%(ReferencePathWithRefAssemblies.Identity)" Condition="%(ReferencePathWithRefAssemblies.ReferenceSourceTarget) == 'ProjectReference'"> | ||
<ClassName Condition="%(ReferencePathWithRefAssemblies.ReferenceSourceTarget) == 'ProjectReference' And %(ReferencePathWithRefAssemblies.ServiceNameOverride) == ''">$([System.IO.Path]::GetFileNameWithoutExtension(%(ReferencePathWithRefAssemblies.Identity)).Replace(".", "_").Replace("-","_").Replace(" ","_"))</ClassName> | ||
<ClassName Condition="%(ReferencePathWithRefAssemblies.ReferenceSourceTarget) == 'ProjectReference' And %(ReferencePathWithRefAssemblies.ServiceNameOverride) != ''">$([System.String]::Copy(%(ReferencePathWithRefAssemblies.ServiceNameOverride)).Replace(".", "_").Replace("-","_").Replace(" ","_"))</ClassName> | ||
<AssemblyName>$([System.IO.Path]::GetFileNameWithoutExtension(%(ReferencePathWithRefAssemblies.Identity)))</AssemblyName> | ||
<ProjectPath>$([System.IO.Path]::GetFullPath(%(ReferencePathWithRefAssemblies.ProjectReferenceOriginalItemSpec)))</ProjectPath> | ||
<Namespace>Projects</Namespace> | ||
<ServiceMetadataSource Include="@(_ServiceAppProject)" Condition="'@(_ServiceAppProject)' != ''"> | ||
<ClassName Condition="%(_ServiceAppProject.ServiceNameOverride) == ''">$([System.IO.Path]::GetFileNameWithoutExtension(%(_ServiceAppProject.Identity)).Replace(".", "_").Replace("-","_").Replace(" ","_"))</ClassName> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we update this from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or maybe just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about I don't like calling it "ResourceName" because that is what we call the string parameter that pass in to
In MSBuild, I haven't seen setting something to empty and not setting it being different behavior. Usually checking for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For Items we do have the 'HasMetadata' item function, but aside from this it can be hard to distinguish between missing and empty. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like setting this metadata to empty to mean something completely different than what the metadata is intended for.
I do think we should rename There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strong agree that "empty metadatum" and "metadatum not specified" should not mean different things--in particular because there's no way to remove metadata other than setting it to empty. |
||
<ClassName Condition="%(_ServiceAppProject.ServiceNameOverride) != ''">$([System.String]::Copy(%(_ServiceAppProject.ServiceNameOverride)).Replace(".", "_").Replace("-","_").Replace(" ","_"))</ClassName> | ||
<ProjectPath>$([System.IO.Path]::GetFullPath(%(_ServiceAppProject.Identity)))</ProjectPath> | ||
</ServiceMetadataSource> | ||
</ItemGroup> | ||
</Target> | ||
|
@@ -32,33 +36,27 @@ | |
<ItemGroup> | ||
<ServiceMetadataSource Update="@(ServiceMetadataSource)"> | ||
<Source> | ||
<![CDATA[using System%3B | ||
<![CDATA[namespace Projects%3B | ||
|
||
namespace ]]>%(Namespace)<![CDATA[%3B | ||
|
||
[System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, ProjectPath = {ProjectPath}")] | ||
public class ]]>%(ClassName)<![CDATA[ : IServiceMetadata | ||
[global::System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, ProjectPath = {ProjectPath}")] | ||
public class ]]>%(ClassName)<![CDATA[ : global::Aspire.Hosting.IServiceMetadata | ||
eerhardt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
public string ProjectPath => """]]>%(ProjectPath)<![CDATA["""%3B | ||
}]]> | ||
</Source> | ||
</ServiceMetadataSource> | ||
</ItemGroup> | ||
|
||
<WriteLinesToFile | ||
File="$(_AspireIntermediatePath)references\%(ServiceMetadataSource.AssemblyName).ServiceMetadata.g.cs" | ||
Overwrite="true" | ||
Lines="%(ServiceMetadataSource.Source)" | ||
Encoding="Unicode" | ||
Condition="%(ServiceMetadataSource.AssemblyName) != ''" | ||
WriteOnlyWhenDifferent="true" | ||
/> | ||
<WriteLinesToFile File="$(_AspireIntermediatePath)references\%(ServiceMetadataSource.ClassName).ServiceMetadata.g.cs" | ||
Overwrite="true" | ||
Lines="%(ServiceMetadataSource.Source)" | ||
Encoding="Unicode" | ||
Condition="%(ServiceMetadataSource.ClassName) != ''" | ||
WriteOnlyWhenDifferent="true" /> | ||
<ItemGroup> | ||
<FileWrites Include="$(_AspireIntermediatePath)references\%(ServiceMetadataSource.AssemblyName).ServiceMetadata.g.cs" /> | ||
<Compile | ||
Include="$(_AspireIntermediatePath)references\%(ServiceMetadataSource.AssemblyName).ServiceMetadata.g.cs" | ||
Condition="%(ServiceMetadataSource.AssemblyName) != ''" | ||
/> | ||
<FileWrites Include="$(_AspireIntermediatePath)references\%(ServiceMetadataSource.ClassName).ServiceMetadata.g.cs" /> | ||
<Compile Include="$(_AspireIntermediatePath)references\%(ServiceMetadataSource.ClassName).ServiceMetadata.g.cs" | ||
Condition="%(ServiceMetadataSource.ClassName) != ''" /> | ||
</ItemGroup> | ||
</Target> | ||
|
||
|
@@ -75,9 +73,7 @@ public class ]]>%(ClassName)<![CDATA[ : IServiceMetadata | |
<ItemGroup> | ||
<ProjectMetadataSource Update="@(ProjectMetadataSource)"> | ||
<Source> | ||
<![CDATA[using System%3B | ||
|
||
namespace ]]>Projects<![CDATA[%3B | ||
<![CDATA[namespace Projects%3B | ||
|
||
internal static class ]]>%(ClassName)<![CDATA[ | ||
{ | ||
|
@@ -87,13 +83,11 @@ internal static class ]]>%(ClassName)<![CDATA[ | |
</ProjectMetadataSource> | ||
</ItemGroup> | ||
|
||
<WriteLinesToFile | ||
File="$(_AspireIntermediatePath)references\_AppHost.ProjectMetadata.g.cs" | ||
Overwrite="true" | ||
Lines="%(ProjectMetadataSource.Source)" | ||
Encoding="Unicode" | ||
WriteOnlyWhenDifferent="true" | ||
/> | ||
<WriteLinesToFile File="$(_AspireIntermediatePath)references\_AppHost.ProjectMetadata.g.cs" | ||
Overwrite="true" | ||
Lines="%(ProjectMetadataSource.Source)" | ||
Encoding="Unicode" | ||
WriteOnlyWhenDifferent="true" /> | ||
<ItemGroup> | ||
<FileWrites Include="$(_AspireIntermediatePath)references\_AppHost.ProjectMetadata.g.cs" /> | ||
<Compile Include="$(_AspireIntermediatePath)references\_AppHost.ProjectMetadata.g.cs" /> | ||
|
@@ -104,14 +98,83 @@ internal static class ]]>%(ClassName)<![CDATA[ | |
<Warning Code="ASPIRE001" Text="The '$(Language)' language isn't fully supported by Aspire - some code generation targets will not run, so will require manual authoring." /> | ||
</Target> | ||
|
||
<!-- | ||
Gets the references in 'AppProjectTargetFramework' that aren't executable projects. | ||
--> | ||
<UsingTask TaskName="GetNonExecutableReferences" | ||
TaskFactory="RoslynCodeTaskFactory" | ||
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll"> | ||
<ParameterGroup> | ||
<AppProjectTargetFramework ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" /> | ||
<NonExecutableReferences ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" /> | ||
</ParameterGroup> | ||
<Task> | ||
<Using Namespace="System.Xml.Linq" /> | ||
<Code Type="Fragment" Language="cs"> | ||
<![CDATA[ | ||
eerhardt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
HashSet<ITaskItem> nonExecutableReferences = new HashSet<ITaskItem>(); | ||
|
||
foreach (var appProject in AppProjectTargetFramework) | ||
{ | ||
var additionalProperties = appProject.GetMetadata("AdditionalPropertiesFromProject"); | ||
if (string.IsNullOrEmpty(additionalProperties)) | ||
{ | ||
// Skip any projects that don't contain the right metadata | ||
continue; | ||
} | ||
|
||
var additionalPropertiesXml = XElement.Parse(additionalProperties); | ||
foreach (var targetFrameworkElement in additionalPropertiesXml.Elements()) | ||
{ | ||
var isExe = targetFrameworkElement.Element("_IsExecutable"); | ||
if (isExe != null && !string.Equals(isExe.Value, "true", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
nonExecutableReferences.Add(appProject); | ||
} | ||
} | ||
} | ||
|
||
NonExecutableReferences = nonExecutableReferences.ToArray(); | ||
]]> | ||
</Code> | ||
</Task> | ||
</UsingTask> | ||
|
||
<!-- | ||
Validates that all the ProjectReferences of an Aspire AppHost project are executables and | ||
informs the developer to set 'AspireHostLibrary=true' if they really intended on ProjectReferencing a library. | ||
--> | ||
<Target Name="_ValidateAspireHostServiceReferences" | ||
joperezr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
DependsOnTargets="_CreateServiceAppProjects" | ||
Condition="'$(IsAspireHost)' == 'true' and '$(SkipValidateAspireHostServiceReferences)' != 'true'"> | ||
|
||
<MSBuild Projects="@(_ServiceAppProject)" | ||
Targets="GetTargetFrameworks" | ||
BuildInParallel="$(BuildInParallel)" | ||
Properties="%(_ServiceAppProject.SetConfiguration); %(_ServiceAppProject.SetPlatform)" | ||
ContinueOnError="!$(BuildingProject)" | ||
RemoveProperties="%(_ServiceAppProject.GlobalPropertiesToRemove);TargetFramework;RuntimeIdentifier;SelfContained;$(_GlobalPropertiesToRemoveFromProjectReferences)" | ||
SkipNonexistentTargets="true"> | ||
<Output TaskParameter="TargetOutputs" ItemName="_ServiceAppProjectTargetFramework" /> | ||
</MSBuild> | ||
|
||
<GetNonExecutableReferences AppProjectTargetFramework="@(_ServiceAppProjectTargetFramework)"> | ||
<Output TaskParameter="NonExecutableReferences" ItemName="_NonExecutableServiceApps" /> | ||
</GetNonExecutableReferences> | ||
|
||
<Warning Code="ASPIRE004" | ||
joperezr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Condition="'@(_NonExecutableServiceApps)' != ''" | ||
Text="'%(_NonExecutableServiceApps.OriginalItemSpec)' is referenced by an Aspire AppHost project, but it is not an executable. Did you mean to set AspireHostLibrary="true"?" /> | ||
</Target> | ||
|
||
<PropertyGroup> | ||
<!-- Easy extension point for adding new languages' write support. --> | ||
<_WriteServiceMetadataSourcesDependsOn>_CSharpWriteServiceMetadataSources;_CSharpAppHostProjectMetadataSources;_WarnOnUnsupportedLanguage</_WriteServiceMetadataSourcesDependsOn> | ||
<WriteServiceMetadataSourcesDependsOn>_CSharpWriteServiceMetadataSources;_CSharpAppHostProjectMetadataSources;_WarnOnUnsupportedLanguage;_ValidateAspireHostServiceReferences</WriteServiceMetadataSourcesDependsOn> | ||
</PropertyGroup> | ||
|
||
<!-- The purpose of this target is to take all of the generated service meta data and write them to the intermediate build directory | ||
and reference them for compilation. There will be a AssemblyName.ServiceMetadata.g.cs file for each referenced project. --> | ||
<Target Name="WriteServiceMetadataSources" DependsOnTargets="$(_WriteServiceMetadataSourcesDependsOn)" BeforeTargets="CoreCompile" /> | ||
<!-- The purpose of this target is to take all of the generated service metadata and write them to the intermediate build directory | ||
and reference them for compilation. There will be a ClassName.ServiceMetadata.g.cs file for each referenced project. --> | ||
<Target Name="WriteServiceMetadataSources" DependsOnTargets="$(WriteServiceMetadataSourcesDependsOn)" BeforeTargets="CoreCompile" /> | ||
|
||
<!-- This target registers the location of the Aspire orchestration dependencies --> | ||
<Target Name="SetOrchestrationDiscoveryAttributes" BeforeTargets="GetAssemblyAttributes"> | ||
|
@@ -157,11 +220,10 @@ internal static class ]]>%(ClassName)<![CDATA[ | |
</ItemGroup> | ||
|
||
<Exec Command="@(_AspireManifestPublishArg, ' ')" | ||
ConsoleToMsBuild="true" | ||
IgnoreStandardErrorWarningFormat="true" | ||
EchoOff="true" | ||
StandardOutputImportance="low" | ||
StandardErrorImportance="low" | ||
/> | ||
ConsoleToMsBuild="true" | ||
IgnoreStandardErrorWarningFormat="true" | ||
EchoOff="true" | ||
StandardOutputImportance="low" | ||
StandardErrorImportance="low" /> | ||
</Target> | ||
</Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: Not really a suggestion, but I'm not sure I love the name of the metadata. From a user's POV it is not obvious what this means until you understand the context. I wonder if we should instead use something more explicit, like
AddReferenceToAssembly
or something that is more obvious.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that I'm not super happy about this name, but I haven't found one I like better yet.
The metadata does more than just take away the reference to the assembly, it is really the switch between "is this an app that you want to orchestrate?" or "is this a library that has AppHost extension logic you want to use?".
Another idea is to flip it to be
AspireServiceApp=false
. We already haveServiceNameOverride
if you want to specify the name of theProjects
class. SettingAspireServiceApp=false
would mean "this isn't an application, so reference it like a normal ProjectReference".There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super nit: Prefixing the name with
Is*
would make it more readable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I think initially I didn't add
Is*
because the other ProjectReference metadata didn't follow this pattern:But these are more "should" and not "is", so I think it is a different scenario.
So should we have:
IsAspireHostLibrary=true
IsAspireServiceApp=false
?
I think I'm leaning towards
IsAspireHostLibrary=true
because the naming aligns withIsAspireHost=true
for the whole project.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really know yet all the special stuff done for these projects, so not sure what a
ServiceApp
vsLibrary
would mean.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ServiceApp is literally any type of project which represents a microservice that you want to launch via the AppHost. This means the AppHost will orchestrate those types of projects.
The AppHost, while it is in reality an orchestrator, in the end is just a C# project, so you could think of scenarios where people want to add project references to libraries too (which aren't really microservices). For example, imagine a company that has several Aspire apps, and they want to share some of the logic they have in their AppHost projects. For this, they would build a shared library, which AppHost projects would all reference. This library, is not really a service, but instead just a library used by the AppHost.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So:
ServiceApp
- special project that the AppHost project will use in some way, maybe post-process etc.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't a "special" project. But ServiceApps are just regular ASP.NET apps - like a web api, or blazor server app. They can also be "Worker" apps, if they don't need to expose an HTTP endpoint.
The AppHost's Program.cs files uses these apps by calling
builder.AppProject<ServiceApp>("serviceName")
.I don't know of this case. In my head there are mainly 2 cases: ServiceApps and "normal" ProjectReferences to libraries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My misunderstanding then.
I would vote for
IsAspireServiceApp=false
because we are changing the defaultProjectReference
to mean a ServiceApp reference which once you know it, makes sense. IOW,AppHost
project referencesServiceApp
s by default. In that case for any other project references an opt-out of that default would make sense.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussing this more with @DamianEdwards, we decided to go with the name:
IsAspireProjectResource=false
to align with the "Resource" terminology in the app model.
We can then do some more renames:
ServiceNameOverride
=>AspireProjectMetadataTypeName
IServiceMetadata
=>IProjectMetadata