Description
Summary
The MSBuild Condition
syntax is very powerful, but it's also very open-ended and hard to create tooling around. There is value in making syntax for common project- and MSBuild-logic constraints more easy-to-use, because a more-restricted but easier-to-use syntax would be both more understandable to users (especially casual MSBuild users) and could disallow entire categories of errors that exist today (e.g. typos in common Condition
usage).
Background and Motivation
Consider a user that wants to make a number of changes to Property values when they are targeting the Release
configuration. For example, they might want to opt in to
- embedded PDB generation
- single-file publishing
- AOT compilation
- certain kinds of warnings/diagnostics
Today, this is all doable. A user might write some code like
<PropertyGroup>
<PdbType Condition="'$(Configuration)' == 'Release'">embedded</PdbType>
<PublishSingleFile>true</PublishSingleFile>
<PublishAot>true</PublishAot>
<NoWarn Condition="'$(Configuration)' == 'Release'">$(NoWarn);MSB12345</NoWarn>
</PropertyGroup>
Things to note here:
- the user has duplicated the condition a few times
- The user has made a decision to set some Properties unconditionally and some conditionally - why might that be?
- The design of the features used (AOT, single-file publishing, etc) has to know about the impact of build-time vs publish-time, so the feature has been implemented with flags that are only 'checked' during Publishing - this requires some knowledge outside of the build!
Making changes
What if the user needs to add a pivot to the condition for any reason? Say for .NET 9 they need an additional NoWarn. The code might look like this:
<PropertyGroup>
<PdbType Condition="'$(Configuration)' == 'Release'">embedded</PdbType>
<PublishSingleFile>true</PublishSingleFile>
<PublishAot>true</PublishAot>
- <NoWarn Condition="'$(Configuration)' == 'Release'">$(NoWarn);MSB12345</NoWarn>
+ <NoWarn Condition="'$(Configuration)' == 'Release' and '$(TargetFramework)' == 'net9.0'">$(NoWarn);MSB12345;NETSDK98765</NoWarn>
</PropertyGroup>
This change is hard to spot-check for most users.
Consolidating changes
Experienced MSBuild users might reach for a Condition
on the PropertyGroup to reduce the amount of duplication on the properties, like so:
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<PdbType>embedded</PdbType>
<PublishSingleFile>true</PublishSingleFile>
<PublishAot>true</PublishAot>
<NoWarn>$(NoWarn);MSB12345</NoWarn>
</PropertyGroup>
This is great! However, many users do not realize that Conditions can be applied to these nodes, and the problem of not being able to easily-classify the changes remains. If you need to add another TFM, now you either have two levels of Condition (one at the PropertyGroup, one on the NoWarn Property) or you have two PropertyGroups.
We need a syntax that is easy to use for common pivots that users experience daily.
Proposed Feature
We should extend the grammar of PropertyGroups and ItemGroups to allow for metadata to be set that the engine would interpret as an automatically-correct Conditions. I propose that the initial set of Properties we consider for this treatment be
- TargetFramework
- Configuration
- RuntimeIdentifier
- Platform
With these properties, users would be able to construct targeted PropertyGroups or ItemGroups that scope changes easily. An example of the above in this fashion:
<PropertyGroup Configuration="Release">
<PdbType>embedded</PdbType>
<PublishSingleFile>true</PublishSingleFile>
<PublishAot>true</PublishAot>
<NoWarn>$(NoWarn);MSB12345</NoWarn>
</PropertyGroup>
During parsing, the engine would read this attribute and generate a Condition on this PropertyGroup equivalent to Condition="$(Configuration) == 'Release'"
. Implicit conditions would be additive - setting both Configuration="Release"
and TargetFramework="net8.0"
would result in a compound Condition of Condition="$(Configuration) == 'Release' and $(TargetFramework) == 'net8.0'"
.
The Groups would be read and applied in-order in the file according to the existing MSBuild passes, so this change would be a purely-semantic transformation - no changes to passes or the resulting project would be intended here.
Open Questions
Why only PropertyGroups/ItemGroups?
Properties cannot have metadata today, and Items already have metadata that may clobber the existing properties - it would be hard to do this in a safe way. It's natural to group changes to properties and items based on these properties, so this extension felt natural.
Is this set of properties sufficient?
We've chosen the common user pivots today, but the syntax isn't limited to/exhaustive. This feels like a kind of change that we should start small on to keep the problem space limited/well-understood. An immediate suggestion to add might be
LangVersion
Should we limit the set of properties at all?
Yes - if we make this change we'll be supporting the syntax forever. We need to be conservative in the set of values accepted here so that we have room to evolve in the future.
Should we support comparisons on things other than Properties?
Item-comparisons are common, but due to the grammar of MSBuild Item-based comparisons can only happen on ItemGroups - is this clear to users? Would it be a foot-gun?
Should the TFM condition sematics be Exactly Equal
or IsTargetFrameworkCompatible
?
The other property checks use exact-matching, and this seems like a safer/more limited default. We could change to use an is-compatible check based on user feedback.
How should this be represented in tooling/API?
The Object Model objects should have a fully-realized Condition
, and there should be a new Well-Known metadata key on ItemGroups and PropertyGroups that provides a tooling-visible representation of the implicit Condition terms and their values.
Other notes
This syntax is very regular and I think could be more easily understood and well-formed-synthesized by LLM tools. The MSBuild language is not well known today by these tools, so something that is more simple and likely to be correct is a plus.
Could this be pluggable in some way?
It feels odd for MSBuild itself to know about specific Properties that may only be known to/used by a specific SDK. Could a given MSBuild SDK somehow provide this data?
Should compound values be allowed in the property comparisons (e.g. TargetFramework="net8.9;net9.0"
)
No - the intent of these comparisons is singular values only.
Alternative Designs
There are other ways to tackle this for sure -
- have entirely different project file format (a la toml/ini/etc) that have a more natural pivoting syntax
- make Property Functions for common conditions and keep the general
Condition
syntax intact