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
Nullability in SignalR #29219
Nullability in SignalR #29219
Conversation
@@ -116,7 +116,7 @@ internal StreamTracker StreamTracker | |||
/// <summary> | |||
/// Gets the user for this connection. | |||
/// </summary> | |||
public virtual ClaimsPrincipal? User => Features.Get<IConnectionUserFeature>()?.User; | |||
public virtual ClaimsPrincipal User => Features.Get<IConnectionUserFeature>()?.User ?? new ClaimsPrincipal(); |
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.
Not sure what to do here. Cache the User
?
The reason I'm not keeping it null is because it's nicer to deal with an empty ClaimsPrincipal
than a null one. An empty one will not be authenticated in any way or have any claims. The Auth types we use don't have null annotations for the user as the expectation is that you'll at least pass an empty one.
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.
Assign blank ClaimsPrincipal
to underlying IConnectionUserFeature
when the property is accessed?
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.
The IConnectionUserFeature
could come from anyone (in theory), and the interface allows a null user. Plus the interface itself could not be on the connection.
Will review when PR is out of draft 🥇 |
@@ -43,7 +43,7 @@ public static Task<TResult> InvokeAsync<TResult>(this HubConnection hubConnectio | |||
/// The <see cref="Task{TResult}.Result"/> property returns a <typeparamref name="TResult"/> for the hub method return value. | |||
/// </returns> | |||
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple overloads with optional parameters", Justification = "Required to maintain compatibility")] | |||
public static Task<TResult> InvokeAsync<TResult>(this HubConnection hubConnection, string methodName, object arg1, CancellationToken cancellationToken = default) | |||
public static Task<TResult> InvokeAsync<TResult>(this HubConnection hubConnection, string methodName, object? arg1, CancellationToken cancellationToken = default) |
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.
Should these be Task<TResult?>
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 think so? That would force callers to always handle null even if they never expect it to be null. If they use a nullable for TResult
then that means they expect a null value and should handle it, and the compiler will let them know that. So seems better to let the user decide.
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.
Fair enough. It's effectively the same choice we made with JSInterop. But we also do not provide any guarantees against it which is crummy.
src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs
Outdated
Show resolved
Hide resolved
@@ -80,7 +78,7 @@ public static bool ReadAsBoolean(this ref Utf8JsonReader reader, string property | |||
throw new InvalidDataException($"Expected '{propertyName}' to be of type {JsonTokenType.String}."); | |||
} | |||
|
|||
return reader.GetString(); | |||
return reader.GetString()!; |
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.
Now that's just a lie
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.
No it isn't, we check above that the token type is a string. If the string is null it would have token type null.
@@ -98,7 +98,7 @@ public static IDisposable On<T1>(this HubConnection hubConnection, string method | |||
|
|||
return hubConnection.On(methodName, | |||
new[] { typeof(T1), typeof(T2), typeof(T3) }, | |||
args => handler((T1)args[0], (T2)args[1], (T3)args[2])); | |||
args => handler((T1)args[0]!, (T2)args[1]!, (T3)args[2]!)); |
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.
Hmm, I wonder if there is a better way to avoid all these bangs.
{ | ||
#pragma warning disable CS8764 // Nullability of return type doesn't match overridden member (possibly because of nullability attributes). |
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.
Should ConnectionId be made nullable on ConnectionContext?
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 really think that people should be able to rely on ConnectionContext.ConnectionId
being non-null. I'm thinking this should just throw if someone tries to access this property before StartAsync()
completes.
This implementation of ConnectionContext.ConnectionId
is already a bit odd in that its setter throws, so I don't think throwing from the getter when the connection hasn't started is a big deal.
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.
people should be able to rely on
ConnectionContext.ConnectionId
being non-null
It's not just if you access it before starting, if you skip negotiation then this will be null as well.
src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/LongPollingTransport.cs
Outdated
Show resolved
Hide resolved
@@ -80,12 +80,12 @@ public override Task RemoveFromGroupAsync(string connectionId, string groupName, | |||
} | |||
|
|||
/// <inheritdoc /> | |||
public override Task SendAllAsync(string methodName, object?[]? args, CancellationToken cancellationToken = default) | |||
public override Task SendAllAsync(string methodName, object?[] args, CancellationToken cancellationToken = default) |
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.
Is this a change from .NET 5? There is an aspnetcore annoucement issue for nullability changes.
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.
This is an API change, however passing null would have thrown
{ | ||
if (_user is null) | ||
{ | ||
_user = Features.Get<IConnectionUserFeature>()?.User ?? new ClaimsPrincipal(); |
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.
👍
42cdc0c
to
5c0693f
Compare
src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs
Show resolved
Hide resolved
@@ -100,7 +99,7 @@ public CookieContainer Cookies | |||
/// <summary> | |||
/// Gets or sets the URL used to send HTTP requests. | |||
/// </summary> | |||
public Uri Url { get; set; } | |||
public Uri Url { get; set; } = default!; |
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.
var httpConnectionOptions = new HttpConnectionOptions();
var urlString = httpConnectionOptions.Url.ToString();
The above throws, right? If so, I think this should be marked nullable.
I might feel differently if it was super common for developer to call the HttpConnectionOptions.Url getter and in practice it was always set, but I expect most developers just call WithUrl() and never touch this property.
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.
The reason I changed it was because of "!
fatigue", everywhere we access this property internally it will be non-null
aspnetcore/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnectionFactory.cs
Lines 59 to 61 in e737b6e
if (_httpConnectionOptions.Url != null && _httpConnectionOptions.Url != uriEndPoint.Uri) | |
{ | |
throw new InvalidOperationException($"If {nameof(HttpConnectionOptions)}.{nameof(HttpConnectionOptions.Url)} was set, it must match the {nameof(UriEndPoint)}.{nameof(UriEndPoint.Uri)} passed to {nameof(ConnectAsync)}."); |
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 do appreciate trying to avoid "!
fatigue" but I don't think that's enough of a reason to misrepresent the nullability of a public property. We could add an internal non-nullable property that mirrors Url and throws if it is null as a sanity check to avoid the fatigue.
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.
We do this in places where something could be null, but for all typical usage it won't be.
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.
Right, that's why I said this initially:
I might feel differently if it was super common for developer to call the HttpConnectionOptions.Url getter and in practice it was always set, but I expect most developers just call WithUrl() and never touch this property.
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 expect most developers just call WithUrl() and never touch this property
Right, that makes it less of an issue if we lie with the API.
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 would argue that if it's not a huge issue either way, we should default to no lying with the API.
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'll try out a private member so we can avoid using !
everywhere in HttpConnection then.
5aa9387
to
39d798f
Compare
Fixes #25612
Fixes #28954