With #309 we've added an implicit define based on the target framework, which is super useful for customers when using conditional compilation.
Unfortunately, this makes it extremely easy to write code that breaks during retargeting. Let's say I'm in a .NET Standard project and I'd like to add a code path that uses some new feature in .NET Standard 2.1, so I multi-target for .NET Standard 2.0 and .NET Standard 2.1. The code would look as follows:
```C#
public void SomeMethod()
{
#if NETSTANDARD2_1
// Write some code that uses Span
#else
// Write fallback logic
#endif
}
Fast forward a year. Now I'd like to add some logic that can light-up on .NET Standard 2.2. So in a different area in my code I'm writing this:
```C#
public void SomeOtherMethod()
{
#if NETSTANDARD2_2
// Write some code that uses some new feature in .NET Standard 2.2
#else
// Write fallback logic
#endif
}
The code will compile just fine and everything looks dandy until you realize that your .NET Standard 2.1 binary no longer uses Span<T>
in SomeMethod()
but emitted the fallback logic.
Ideally, we'd want to write code like this:
```C#
public void SomeMethod()
{
#if NETSTANDARD >= 2.1
// Write some code that uses Span
#else
// Write fallback logic
#endif
}
Which C# doesn't support (and likely never will). However, we could instead change the SDK to define more symbols, such as:
```C#
public void SomeMethod()
{
#if NETSTANDARD2_1_OR_HIGHER
// Write some code that uses Span<T>
#else
// Write fallback logic
#endif
}
The _OR_HIGHER
symbols would all be defined.
Thoughts?
Tagging @dotmorten who pointed this out to me.
Also tagging @jaredpar and @MadsTorgersen for thoughts on the limitation in the C# preprocessor
Here's my specific scenario. Let's say I write code that targets NET452 specifically:
public void SomeMethod()
{
#if NET452
// Write some code that uses Span<T>
#endif
}
Fast forward a few years where 4.5.1 is no longer supported and I upgrade my target to .NET 461. Now I have to go through all places where I use NET451
and change them to NET461
.
Again it would have been more useful to just say #if NETFX>461
or something like that.
Personally I'd prefer being able to define numbers (and/or versions), and use =><
operators. This also comes handy when dealing with compiler differences, so I can switch based on which compiler version is used. This has historically been very useful for C++.
The other beauty of this is, if I write #if NETSTANDARD >= 2.0
the code very clearly reads to any developer "this code is supported from .NET Standard 2.0 and up".
This would help 3rd-party devs too. Unity uses the same suggested solution, it defines both e.g. UNITY_5_6_1
and UNITY_2017_3_OR_NEWER
versions, but numbers would simplify that a whole lot as well. (As it stands they have a magnitude more of these defines as their release cadence is quicker.)
If we're pushing for a C# change it's worth pointing out numeric symbols won't work for cases where we have three digit version numbers (net > 4.5.1
). MSBuild has this problem today where two digits end up being compared how you would expect while three digits are ending up being a string comparison and thus 4.5.10 isn't considered larger than 4.5.9. Oops.
@terrajobst it'd be nice to have support for that. If major rework has to happen in the C# preprocessor anyway why not go all the way and have support for three digit version numbers (and maybe more) as well?
MSBuild has this problem today where two digits end up being compared how you would expect while three digits are ending up being a string comparison and thus 4.5.10 isn't considered larger than 4.5.9. Oops
This isn't true. Msbuild will try System.Version before going to string. It will see 4.5.10 > 4.5.9
IIRC, where it can fail like this is with two part versions because it tries double before System.Version.
Fast forward a few years where 4.5.1 is no longer supported and I upgrade my target to .NET 461. Now I have to go through all places where I use NET451 and change them to NET461.
If you only need to target one .NET Framework version, use #if NETFRAMEWORK
(NETSTANDARD
exists too if you only need to target one .NET Standard version)
A better solution for this would be awesome. For a long time this has been a pain point for me, so I stopped using the automatic framework defines and started using manual per-feature defines in the csproj, but this gets super messy too, and is hard to maintain.
Even doing this in msbuild to change DefineConstants
feels a bit hacky since the required property is ""private"" - _TargetFrameworkVersionWithoutV
. See https://github.com/dotnet/sdk/issues/2618.
For https://stackoverflow.com/questions/44208788/conditionally-compiling-for-a-minimum-version-of-net/44209523#44209523, this condition was the best i could come up with but it hurts my eyes
Condition="('$(TargetFrameworkIdentifier)' == '.NETStandard' and '$(TargetFrameworkVersion.Substring(1))' >= '1.4')"
Plus the TargetFrameworkVersion
is defined in the SDK targets so pretty unusable in user projects if you don't want to do explicit SDK imports and add stuff after Sdk.targets
- to get something copy-&pasteable it has to be done inside a target.
Most helpful comment
If we're pushing for a C# change it's worth pointing out numeric symbols won't work for cases where we have three digit version numbers (
net > 4.5.1
). MSBuild has this problem today where two digits end up being compared how you would expect while three digits are ending up being a string comparison and thus 4.5.10 isn't considered larger than 4.5.9. Oops.