manuelroemer / Nullable

A source code only package which allows you to use .NET's nullable attributes in older target frameworks like .NET Standard 2.0 or the "old" .NET Framework.
https://www.nuget.org/packages/Nullable
MIT License
186 stars 8 forks source link

[Solution] Nullable attributes are not discovered in WPF projects #11

Closed chucker closed 3 years ago

chucker commented 4 years ago

I have a relatively complex solution with lots of legacy stuff, but I did manage to modernize some of the assemblies to the Sdk style and PackageReference instead of packages.config. I can add your NuGet package and then write code like this, in .NET Framework 4.7.2:

    public bool TryGetExistingFavorite(ContactRecord record, [NotNullWhen(true)] out Favorite? favorite)
    {
        favorite = _Favorites.FirstOrDefault(f => f.CID == record.ID);

        return favorite != null;
    }

This looks great during design-time builds. But when I actually do a full build, I get an error that the attributes can't be found:

1>…path…\Favorites\FavoritesRepository.cs(108,68,108,79): error CS0246: The type or namespace name 'NotNullWhenAttribute' could not be found (are you missing a using directive or an assembly reference?)
1>…path…\Favorites\FavoritesRepository.cs(108,68,108,79): error CS0246: The type or namespace name 'NotNullWhen' could not be found (are you missing a using directive or an assembly reference?)
1>Done building project "MyProject_hh54oelh_wpftmp.csproj" -- FAILED.

I've tried both a regular reference like so:

    <PackageReference Include="Nullable" Version="1.2.1" />

And the adjusted one from the README:

    <PackageReference Include="Nullable" Version="1.2.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>

I think the …**wpftmp**.csproj portion may be a clue. However, with a test project that also contains WPF, I wasn't able to reproduce the issue.

manuelroemer commented 4 years ago

Hey, sorry to hear that you are having this issue. I've done some testing/browsing and can reproduce it locally (Project Download Here - open MainWindow.xaml.cs for the method triggering the problem).

It seems that the issue arises from the way WPF projects are compiled. There are some GitHub issues which may provide more insights into why the problem occurs:

To summarize the issue: During the build, WPF creates a temporary project (for compiling XAML, I believe). In this temporary project, the NuGet packages are not imported, hence the build error that the attributes could not be found. This problem also affects other source code only packages similar to Nullable.

Unfortunately I am currently unsure how this might be fixed. Maybe there's a chance to develop a workaround, but ultimately, the issue should be fixed at the root, i.e. in the WPF build process.

Nontheless, I don't want to leave you hanging - maybe there are other ways to solve your problem until a real solution arrives for this issue. Here are some suggestions:

1) You might want to have a look at the https://github.com/tunnelvisionlabs/ReferenceAssemblyAnnotator project. They also offer a way to include the nullable attributes in a legacy projects. I haven't tried, but perhaps their approach works better? 2) This is nasty after all the work you have put into upgrading your project, but maybe you can try going back to a non-SDK style project. Again, not sure if this solves the problem, but it might be worth a try. 3) [The Approach I'd Use] You could not use the Nullable package for WPF projects and instead manually define the attributes using a shared project. This should work with any project configuration. This is also not ideal since you lose automatic package updates, but realistically, there won't be many since the .NET team doesn't periodically create new attributes. If anything, #9 will be added soon-ish, but that's it for the next year(s).

If you (or anyone else) has any ideas how the problem can be worked around from the perspective of the Nullable package, feel free to share your ideas!

chucker commented 4 years ago

(Thanks for your answer. Will look into those!)

chucker commented 4 years ago

So, I tried approach 1, and… they have a similar issue (which sounds… more fixable?):

error CS0122: 'NotNullWhenAttribute' is inaccessible due to its protection level

It looks like someone else already discovered that: https://github.com/tunnelvisionlabs/ReferenceAssemblyAnnotator/issues/81

2 seems… not fun.

I might be able to live with 3. But now I'm tempted by the additional features in the project mentioned in 1…

manuelroemer commented 4 years ago

Alright, thanks for testing! I'm not sure about the CS0122 being more fixable. On first sight, the only solution seems to be to make the attributes public, but that should never be done, because it will quickly lead to naming conflicts when another referencing project also declares these attributes. Hence why they are declared as internal. (The only exception where public attributes are okay-ish is a project which is not published - there you have full control over it.)

now I'm tempted by the additional features in the project mentioned in 1…

I totally recommend using ReferenceAssemblyAnnotator instead of Nullable then! It is indeed an awesome project.
Nullable is kept lightweight on purpose, i.e. I only want to provide the attributes and nothing else. Using an analogy, Nullable is, so to say, the lightweight code editor while ReferenceAssemblyAnnotator is a fully featured IDE - sometimes the first is enough, but for larger tasks/projects, the additional features are preferable. 😄

If they cannot fix the CS0122 error which you posted above, you might even be able to combine ReferenceAssemblyAnnotator with Option 3 above, i.e. you could manually create the NullableAttributes.cs file in your project to have access to the attributes and disable RAA's attribute generation in your cs project: <GenerateNullableAttributes>false</GenerateNullableAttributes>. Not tested, but this might work just fine for your project.

chucker commented 4 years ago

Nullable is kept lightweight on purpose, i.e. I only want to provide the attributes and nothing else.

Yeah, makes sense to me. This is the one I ran into, but I think I want the "IDE" approach for my use case.

They did figure out a fix, which appears to work for me (once I explicitly set UseWPF). That fix may actually help you as well. It appears _GeneratedCodeFiles is a variable used by the WinFX (i.e. WPF) tasks.

AArnott commented 3 years ago

Check out https://github.com/microsoft/CsWin32/issues/7 for a discussion of this and how the latest .NET SDK fixed their bug so Nullable should Just Work going forward.

manuelroemer commented 3 years ago

That sounds amazing! I’ll close this as resolved then and make a mental note to document this in the README so that it is immediately clear to WPF users!