Skip to content
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

Add option to generate C# nullable annotations #6632

Open
YairHalberstadt opened this issue Sep 10, 2019 · 37 comments
Open

Add option to generate C# nullable annotations #6632

YairHalberstadt opened this issue Sep 10, 2019 · 37 comments

Comments

@YairHalberstadt
Copy link

Language : C#

Feature Request

C# 8.0 adds support for nullability annotations.

It would be helpful if the the generated protobuf objects added these annotations. This would provide warnings if people tried to dereference a possibly null value, or for example, tried to assign null to a string property of a protobuf object.

At first sight it looks like the rules for which types can be nullable and which can't should be quite simple.

I think the first stage would be annotating the Protobuf Nuget Packages.

Of course nullable annotaions are a compile error in earlier versions of C# than c# 8.0. Therefore this has to be off by default, and triggered by a command line option.

It would be helpful if this was added for GRPC as well.

@jtattermusch
Copy link
Contributor

I'm not generally opposed to having this, but here are the challenges:

  1. we would need a clear design - feel free to provide more thoughts here if you want
  2. currently this is not a priority so there's no ETA and implementation would need to be contributed by someone from the community (but we'd need a design for this feature first).
  3. C# 8 nullable annotations are a brand new C# 8 feature, but we definitely need to maintain the backward compatibility - which probably also means that it's going to take a while before this can actually be used.

There are two separate areas:

  1. annotations in the Google.Protobuf runtime (=the nuget) - then concern is backward-compatibility of the APIs (you cannot have an "option" for enabling/disabling this so it would need to work for everyone - and it's going to take a very long time until everyone can use C# 8).
  2. annotations in the generated code (you can have a codegen option to enable the default-off feature), the question is how useful these checks would be - it's generated code and most of it is just field accessors, so I'm not sure we'd gain much here (a more detailed design would help).

@YairHalberstadt
Copy link
Author

annotations in the Google.Protobuf runtime (=the nuget) - then concern is backward-compatibility of the APIs (you cannot have an "option" for enabling/disabling this so it would need to work for everyone - and it's going to take a very long time until everyone can use C# 8).

The annotations are backwards compatible, so I don't think this should be an issue.

annotations in the generated code (you can have a codegen option to enable the default-off feature), the question is how useful these checks would be - it's generated code and most of it is just field accessors, so I'm not sure we'd gain much here (a more detailed design would help).

A very common mistake I make is to set a string to null. That would be the single most useful gain.

With GRPC specifically, I often by mistake return null from the server implementation. That would be another gain but of course that is for a different repository.

I don't know a huge amount about protoc but my initial design would be:

All string properties are marked non-null.
No change in struct properties.
All collection properties (repeated, or maps) are marked as non-null.
All other properties are marked as nullable.

Parser property marked as non-null.
Descriptor property marked as non-null.

Constructor parameter is non-null
Clone method returns non-null
Equals parameter is nullable
Mergefrom parameter is nullable
MergeFrom(CodedInputStream) parameter is non-null
WriteTo parameter is non-null

@ObsidianMinor
Copy link
Contributor

Instead of using the normal C# 8 syntax perhaps we could make use of the code analysis attributes, that way the syntax works no matter what version of C# you're using. Of course that'd require that a separate package (like Microsoft.Bcl.Async) for everything under .NET Standard 2.1 but should that ever happen it could probably be done that way.

@YairHalberstadt
Copy link
Author

@ObsidianMinor

I think that would be highly undesirable. C# 8.0 introduced nullability as a first class feature, and the amount of work that's been put in is phenomenal (probably more than any other feature in the history of C#). Even then it leaves a large number of holes.

Existing nullability attributes are extremely limited in what they can express, and enforce, and would fail to integrate well with C# 8.0. I think that it's better to make nullability work properly for a subset of users than to make it work badly for a larger subset.

@ObsidianMinor
Copy link
Contributor

ObsidianMinor commented Sep 16, 2019

Were those attributes not specifically added for C# 8? dotnet/corefx#37826 says otherwise. They work with C# 8 flawlessly.

public string TypeUrl {
  get { return typeUrl_; }
  [global::System.Diagnostics.CodeAnalysis.DisallowNullAttribute]
  set {
    typeUrl_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
  }
}

// .. somewhere far far away
#nullable enable

any.TypeUrl = null; // Cannot convert null literal to non-nullable reference type

All of the cases you mentioned work with these attributes.

String properties => DisallowNullAttribute.
All collection properties (repeated, or maps) => NotNullAttribute.
All other properties => MaybeNullAttribute.

Parser property => NotNullAttribute.
Descriptor property => NotNullAttribute.

Constructor parameter => DisallowNullAttribute
Clone method => NotNullAttribute
Equals parameter => AllowNullAttribute
MergeFrom parameter => AllowNullAttribute
MergeFrom(CodedInputStream) => DisallowNullAttribute
WriteTo => DisallowNullAttribute

@YairHalberstadt
Copy link
Author

@ObsidianMinor

I see I misunderstood which attributes you were referring to. My apologies.

The C# 8 nullable annotations emit attributes themselves. The coreFX attributes are meant for when the behaviour is to complex to be described by these annotations.

In so far as pre C# 8 tooling understands the CoreFX attributes, I would expect it to understand the attributes emitted by the compiler. However I strongly doubt either would work well.

@ObsidianMinor
Copy link
Contributor

ObsidianMinor commented Sep 18, 2019

I can probably get a prototype of this working pretty easily but it could only be pulled when and if dotnet/runtime#30801 happens.

Using these annotations would allow us to do this in a backwards compatible way. The behavior of nulls in generated code and the library is too complex to be done with standard ? and ! syntax. So rather than doing it using that syntax, we can express what we need using the attributes. This has the included bonus of working on other lang versions. Only downside is this requires the nuget package as shown above.

@YairHalberstadt
Copy link
Author

Using these annotations would allow us to do this in a backwards compatible way.

Ah you mean to avoid a switch? That sounds like a decent idea.

@ObsidianMinor
Copy link
Contributor

Alright I wrote a prototype (untested) on my fork on the csharp/nullability branch. As it turns out nullability attributes do not affect code analysis in function bodies as noted in dotnet/roslyn#36039 so depending on whether the attributes affect code analysis in the future we may or may not have to disable warnings on the file level which is a very simple addition. Again, this will only work if a separate nuget package is made for these attributes, it works on the prototype since I manually copy-pasted the attributes into Google.Protobuf.

@jtattermusch
Copy link
Contributor

I don't think we'd want to add another dependency to be able to reference the attributes.

@LiamMorrow
Copy link

LiamMorrow commented Jan 30, 2020

This seems like a fairly decent compromise in reference the the idea of not wishing to add another dependency:
https://www.nuget.org/packages/Nullable/
Essentially does what @ObsidianMinor did where the attributes get compiled into the application without having to copy them into the repo. Unless the preference is to not have any new dependencies, regardless of if they then cause a dependency downstream to consuming projects.

@ObsidianMinor , it mightn't even be necessary to have the nuget package, if those attributes which were copied in are made internal then there are no problems with conflicts and it will still allow us to build against older dotnet versions which are not null context aware

@LiamMorrow
Copy link

This is an interesting problem since it would be a breaking change (not so much binary breaking as source breaking). Consider a project with nullable enabled that has code like this:

message FooMessage {
  google.protobuf.StringValue bar = 1;
}
void DoSomething(FooMessage foo) 
{
    string extractedBar = foo.Bar;
    // do something with that string
}

Previously that would have emitted no warnings, but with a change like this the compiler will now say that we are possibly assigning null to a non nullable (extractedBar). Now clearly this is the desired behavior (this is the whole point of nullable reference types). The issue is when we look at this scenario in a project which has TreatWarningsAsErrrors enabled. When we update the tooling reference, our project will no longer compile. This would have a knock on effect with libraries that export their protobuf types (thinking gax etc.).

I'm strongly in favor of having the generator output nullable annotations, however this seems like it could have larger consequences than meets the eye, and thought it would be worth mentioning for discussion since an official proposal has not been done up.

@YairHalberstadt
Copy link
Author

@LiamMorrow
That is true of every single library. The official line of the C# team is: nullable reference types will be disabled by default till .Net 5. If you enable them earlier, expect some churn till then whilst libraries switch NRTs on.

@LiamMorrow
Copy link

Very true @YairHalberstadt . I should have put a note I was mentioning it more of a justification to others (and my future self) for why it will likely be a while before this is turned on by default. I am still interested in the option of a flag to enable it for generation. Seems like it could be a good stopgap in the mean time.

@Falco20019
Copy link
Contributor

We are also interested in this feature. We currently look into enabling nullables and being able to differentiate between string and google.protobuf.StringValue would be a great benefit for us.

I fully understand that this will take some more time, especially since most people won't enable it until it has a better adoption in libs. Just wanted to give it an upvote.

@thefringeninja
Copy link

In the meantime, could we have the auto generated code insert #nullable disable at the top of each generated file?

@jtattermusch
Copy link
Contributor

@thefringeninja I think adding '#nullable disable' to the generated files would actually break compilation on older compilers (since they don't know what #nullable means) and that's completely unacceptable (unless you can somehow include #nullable conditionally).

@thefringeninja
Copy link

I'm not sure about .net framework, but all the dotnet core versions that don't support #nullable are already EOL.

Even so, a flag to enable this would be more than sufficient.

@jtattermusch
Copy link
Contributor

jtattermusch commented Apr 21, 2020

I'm not sure about .net framework, but all the dotnet core versions that don't support #nullable are already EOL.

Not true, .NET Core 2.1 is supported until August 2021 and it doesn't support nullable reference types.
Even if it did, Google.Protobuf supports everything starting from net45 and we cannot just break the generated code for existing users.

Even so, a flag to enable this would be more than sufficient.

New protoc flags are pain to add, maintain, integrate and they also make using protoc complicated, so the we're pretty strict about not adding more flags unless they are absolutely necessary. If we introduced a flag for every feature we would have 1000 flags right now.

Under these restrictions, is there a way to add the #nullable disable conditionally so it doesn't break older compilers?

@jtattermusch
Copy link
Contributor

One idea is to add (if there is really no way to detect whether the compiler supports the #nullable define)

#if GOOGLE_PROTOBUF_DISABLE_NULLABLE_REFERENCES
// For now Google.Protobuf doesn't have special support for nullable references,
// but this at least allow projects that already use the nullable reference feature from C# 8
// to include protobuf generated code more easily.
#nullable disable
#endif

Would that be something that is helpful? (It would require the project to define the GOOGLE_PROTOBUF_DISABLE_NULLABLE_REFERENCES property).

@jtattermusch
Copy link
Contributor

CC @JamesNK

@YairHalberstadt
Copy link
Author

@jtattermusch

Why not use that same technique, but to add nullable annotations in the correct places, guarded by an ifdef?

It would make the generated code much longer and uglier, but I don't know if that's a concern.

@jtattermusch
Copy link
Contributor

@jtattermusch

Why not use that same technique, but to add nullable annotations in the correct places, guarded by an ifdef?

It would make the generated code much longer and uglier, but I don't know if that's a concern.

The difference is that what I'm proposing is a pretty trivial fix that could potentially unlock lot of users. What you're proposing is a significant time investments and it would need to be designed and tested carefully.

Besides that, much longer and uglier code is definitely a concern.

@mtrtm
Copy link

mtrtm commented Sep 1, 2020

Did this thread end up dying? As a .Net developer, I was shocked to learn nullable wasn't supported. There were some arguments a year ago about c# 8, but we are about to be on 9. I would love if this could be implemented - nullable is an incredibly positive option to be able to turn on.

@Falco20019
Copy link
Contributor

Falco20019 commented Oct 23, 2020

@jtattermusch We are also waiting and hoping for this as it's blocking us from enabling nullable annotations in our code base without getting lot's of errors or enabling nullability in each of our files manually. AFAIK there is sadly no way to disable nullability just for a folder (like obj). Only per file or per project. Due to the modular architecture of our code, we can not put the protos in a separate project as this would make our code base multiply heavily.

I hope this gets some more attention with .NET 5.

@SonnyX
Copy link

SonnyX commented May 3, 2021

#7382 has seemingly been merged, just waiting for a new release.

@Falco20019
Copy link
Contributor

Falco20019 commented Jun 18, 2021

@SonnyX Are you sure that's related? I think that's only about optional-marked fields.

@jtattermusch C#9 is around the corner. Any plans or a roadmap here? Is grpc/grpc-dotnet#606 the leading ticket on that?

@Falco20019
Copy link
Contributor

Falco20019 commented Jan 13, 2022

As of the important box in https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references#nullable-contexts this would need to explicitly set #nullable enable since it has the <auto-generated> tag in it's file comment. Just wanted to leave this in reference to #6632 (comment) since it will be disabled automatically, even with the <Nullable>enable</Nullable> being automatically defined in the csproj templates for .NET 6.

Did some more tests recently, since we plan to switch to .NET 6 with nullability soon. It's still a bit disappointing that this is missing, but at least, it's not generating any warnings.

Pinging @jtattermusch and @JamesNK since this is related to grpc/grpc#20729

@JohnGalt1717
Copy link

As of 3 weeks from now .NET 4.8 and earlier are all unsupported. .NET 5 will also be unsupported and all versions of .NET Core that don't support nullable types are already unsupported.

If Google insists on supporting stuff that Microsoft themselves do not, please just add a positive flag that we can set in the csproj file that once set, generates classes with nullable types. If it doesn't have optional, then it isn't nullable. Full stop. If the positive flag isn't there, then emit the old stuff.

This is a major source of bugs and presents a massive barrier to adoption. I simply won't allow code that isn't nullable aware into our code chain anymore specifically because of the runtime errors that allowing it makes. Given that Dart's approach to this topic and being absolutists about it, I think it's fair to approach C# the same way. If people don't want to use nullable types, then they can stick with a previous version. And since the demark is easy (.net Core 3.1 or .net 5) it's easy to exclude them from newer versions.

@cwe1ss
Copy link

cwe1ss commented Apr 27, 2022

As of 3 weeks from now .NET 4.8 and earlier are all unsupported. .NET 5 will also be unsupported and all versions of .NET Core that don't support nullable types are already unsupported.

I'd love to see nullable-support as well, but I don't think your statements regarding End of Support for older .NET versions are correct:

@JohnGalt1717
Copy link

@cwe1ss .NET Core 3.1 supports nullables. .NET 5 supports nullables and is dead in 2 weeks.

.NET 4.6.1 and earlier (with the notable exception of which 0% of people using 3.5 will ber using GRPC) is dead in the same 2 weeks and 4.6.2+ you can shim to make it work, and by all means keep using the old version, or as I suggested just have a flag for what gets outputted and make it a positive output and give guidance on how to put that flag in the csproj file.

IF you wait for no one to be using something, then you'll never improve. We're at the point where the fringe that doesn't have support for nullable types is so small AND very unlikely to be using GRPC that it's safe to say that you could just go all in, but if Google is worried, then just add a flag like they did with optional in the first place but for .NET only and this time document how to pass it from the csproj properly.

@runenilsenoe
Copy link

Still waiting for this

@ps-weber
Copy link

ps-weber commented Mar 30, 2023

I think the problem most people are interested in is avoiding assigning null to string fields.

As far as I can see this could simply be done with the DisallowNullAttribute behind a preprocessor check:

#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
        [global::System.Diagnostics.CodeAnalysis.DisallowNullAttribute]
#endif
        public string MyStringField
        ...

This should be a simple, backwards compatible change that only affects code with nullable analysis enabled.

@ogxd
Copy link

ogxd commented Jul 5, 2023

I've made a PR that implements the solution proposed by @Falco20019, which is in my opinion the most reasonable and effective option (no breaking changes, backward compatible, and optional)
#13218

@tonydnewell
Copy link
Contributor

A gRFC has been published to discuss:
https://github.com/grpc/proposal/blob/master/L110-csharp-nullable-reference-types.md

Discussion:
https://groups.google.com/g/grpc-io/c/R1agzzcmKiE/m/4-HQgggrAgAJ

@JohnGalt1717
Copy link

Whoot whoot!

@Machibuse
Copy link

Any news here? it would be great to have this feature!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests