-
Notifications
You must be signed in to change notification settings - Fork 218
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
Nullable partial work #183
Conversation
@@ -17,7 +17,7 @@ public static class AccountExtensions | |||
/// </summary> | |||
/// <param name="account">The IAccount instance.</param> | |||
/// <returns>A <see cref="ClaimsPrincipal"/> built from IAccount.</returns> | |||
public static ClaimsPrincipal ToClaimsPrincipal(this IAccount account) | |||
public static ClaimsPrincipal? ToClaimsPrincipal(this IAccount? account) |
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.
Public API changes are backwards compatible.
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.
Here and in similar methods, we can also use attributes like `[return: NotNullIfNotNull("account")]
@@ -16,7 +16,7 @@ public static class ClaimsPrincipalExtensions | |||
/// </summary> | |||
/// <param name="claimsPrincipal">Claims principal.</param> | |||
/// <returns>A string corresponding to an account identifier as defined in <see cref="Microsoft.Identity.Client.AccountId.Identifier"/>.</returns> | |||
public static string GetMsalAccountId(this ClaimsPrincipal claimsPrincipal) | |||
public static string? GetMsalAccountId(this ClaimsPrincipal 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.
If a consuming application also enforces the "nullable reference" feature, then they will get warnings when they try to pass in a NULL, for example a null ClaimsPrincinpal here.
If the consuming application does not enforce this feature, they will not get warnings.
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.
How do we decide if we should change the parameter to nullable or keep it non-nullable and throw the ArgumentNullException?
In this case, a null claimsPrincipal can return a null ID value and non-null claimsPrincipal returns non-null ID value.
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.
At line 23 we throw an ArgumentNullEx
if the claimsPrincipal is null, we don't return null.
@@ -16,7 +16,7 @@ public static class ClaimsPrincipalExtensions | |||
/// </summary> | |||
/// <param name="claimsPrincipal">Claims principal.</param> | |||
/// <returns>A string corresponding to an account identifier as defined in <see cref="Microsoft.Identity.Client.AccountId.Identifier"/>.</returns> | |||
public static string GetMsalAccountId(this ClaimsPrincipal claimsPrincipal) | |||
public static string? GetMsalAccountId(this ClaimsPrincipal claimsPrincipal) | |||
{ | |||
if (claimsPrincipal == null) |
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.
From a code perspective, we still need to make null checks and program defensively, because NULLs can still be passed everywhere.
<SignAssembly>true</SignAssembly> | ||
<AssemblyOriginatorKeyFile>../../build/MSAL.snk</AssemblyOriginatorKeyFile> | ||
<GenerateDocumentationFile>true</GenerateDocumentationFile> | ||
<Nullable>enable</Nullable> |
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 how the feature is enabled. It needs C# 8, which is available with .net core 3.1 or with .net class 4.smth (4.7.2?)
@@ -61,7 +61,12 @@ internal class RegisterValidAudience | |||
SecurityToken securityToken, | |||
TokenValidationParameters validationParameters) | |||
{ | |||
JwtSecurityToken token = securityToken as JwtSecurityToken; | |||
JwtSecurityToken? token = securityToken as JwtSecurityToken; | |||
if (token == null) |
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 the kind of bugs you discover when you turn this feature on. THe compiler does NULL analysis and points out areas of the code that are potentially unsafe.
@@ -446,9 +444,9 @@ private async Task<IConfidentialClientApplication> BuildConfidentialClientApplic | |||
/// <param name="tenant"></param> | |||
private async Task<string> GetAccessTokenOnBehalfOfUserFromCacheAsync( |
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.
You can see how the methods become more expressive - you now know which input params can be null and which ones can't.
IEnumerable<string> scopes, | ||
string tenant) | ||
string? tenant) | ||
{ | ||
if (scopes == null) |
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.
But you still have to code defensively...
|
||
/// <summary> | ||
/// Key section on the configuration file that holds the scope value. | ||
/// </summary> | ||
public string ScopeKeySection { get; set; } | ||
public string? ScopeKeySection { get; set; } | ||
|
||
/// <summary> | ||
/// Handles the MsalUiRequiredException. | ||
/// </summary> | ||
/// <param name="context">Context provided by ASP.NET Core.</param> | ||
public override void OnException(ExceptionContext context) |
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 rewrote this method a bit, but all changes are non-breaking (done with analyzers)
@@ -128,9 +124,13 @@ private bool CanBeSolvedByReSignInOfUser(MsalUiRequiredException ex) | |||
OidcConstants.ScopeProfile, | |||
}; | |||
|
|||
// TODO: scopes can actually be null here - how do we treat this case? |
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 the kind of bugs that can be discovered. In this particular case I am not sure how to handle it - please advise.
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.
yes...good point. This is for incremental consent, so we technically should only be here if we have scopes, because that's the point, that additional scopes are required. So I think in OnException()
, we should make sure we have something. @jmprieur thoughts?
In reply to: 433215274 [](ancestors = 433215274)
Thanks @bgavrilMS. Is there an official Microsoft doc that talks about this pattern, plus when to use it or not, etc.? |
Thanks @bgavrilMS I will try to have a look later today. I think this is definitely worth exploring and thanks for you time thus far. |
@pmaytak - sorry I didn't add the official guidance - https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references |
Thanks @bgavrilMS for starting this conversation. I like that the API shows what can be null and not, and that's not a breaking change (except a renaming you've done) Did you notice that the branch does not build? Do you think customers will understand the subtlety? |
@jmprieur - I broke a unit test related to json handling... |
return (T)JsonSerializer.Create().Deserialize(reader, typeof(T)); | ||
using MemoryStream stream = new MemoryStream(jsonByteArray); | ||
using StreamReader reader = new StreamReader(stream, Encoding.UTF8); | ||
return (ClientInfo)JsonSerializer.Create().Deserialize(reader, typeof(ClientInfo)); |
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'm not sure I understand why you replaced the generic here (T) by ClientInfo?
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.
JSON deserialization can always return NULL (since you don't know what you deserialize). But a nullable value type is not the same as a nullable reference type. So general purpose serialization helpers that just take a "T" won't work. I think @pmaytak 's suggestions (where T : notnull
) might do it, I didn't research this enough.
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.
But from a code perspective, why have a general purpose deserialization method in a class that only deserializes ClientInfo? Makes the code hard to read...
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup Label="Package Metadat"> |
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.
Metadat? or Metadata?
@@ -199,7 +197,7 @@ internal class TokenAcquisition : ITokenAcquisition, ITokenAcquisitionInternal | |||
/// OpenIdConnectOptions.Events.OnAuthorizationCodeReceived.</remarks> | |||
public async Task<string> GetAccessTokenForUserAsync( | |||
IEnumerable<string> scopes, | |||
string tenant = null) | |||
string? tenantId = null) |
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 a breaking change. But that's probably ok. We'd document it.
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.
Ah, you mean the rename? Didn't realize, but you are right. I have an analyzer that flaged it up because the name of the param on the interface != name of the param on the class. That analyzer is SQ btw...
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.
Looks fine to me, bogdan. thanks for all the effort. You still have a few hours of this? thanks again. In reply to: 637603887 [](ancestors = 637603887) |
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 think we should move forward with this.
From the docs below, .NET 5 will fully implement Nullable Reference Types (NRTs). The advisory for libraries is to start implementing NRTs before .NET 5 is released. And there's a possibility that NRTs will be the default for new Visual Studio projects starting with .NET 5.
I think the good thing about NRTs is that it forces the developer to think about the intent of the potential null values in code and does add more expressiveness to the public API.
Along with that we do have to focus more on the intent of our code as related to nulls - do we use nullable or non-nullable return values and parameters, do we throw Argument Null Exceptions, do we use NRT attributes for nuanced behaviors. Related quote from one of the articles:
Updating your library for nullable references requires more than sprinkling ? on some of the variables and type names. The preceding example shows that you need to examine your APIs and consider your expectations for each input argument. Consider the guarantees for the return value, and any out or ref arguments upon the method's return.
One question that I still have is how do we decide if we should change the parameter to nullable or keep it non-nullable and throw the ArgumentNullException
?
More resources:
Embracing nullable reference types
Update libraries to use nullable reference types and communicate nullable rules to callers
Reserved attributes contribute to the compiler's null state static analysis
Try out Nullable Reference Types
@@ -17,7 +17,7 @@ public static class AccountExtensions | |||
/// </summary> | |||
/// <param name="account">The IAccount instance.</param> | |||
/// <returns>A <see cref="ClaimsPrincipal"/> built from IAccount.</returns> | |||
public static ClaimsPrincipal ToClaimsPrincipal(this IAccount account) | |||
public static ClaimsPrincipal? ToClaimsPrincipal(this IAccount? account) |
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.
Here and in similar methods, we can also use attributes like `[return: NotNullIfNotNull("account")]
@@ -16,7 +16,7 @@ public static class ClaimsPrincipalExtensions | |||
/// </summary> | |||
/// <param name="claimsPrincipal">Claims principal.</param> | |||
/// <returns>A string corresponding to an account identifier as defined in <see cref="Microsoft.Identity.Client.AccountId.Identifier"/>.</returns> | |||
public static string GetMsalAccountId(this ClaimsPrincipal claimsPrincipal) | |||
public static string? GetMsalAccountId(this ClaimsPrincipal 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.
How do we decide if we should change the parameter to nullable or keep it non-nullable and throw the ArgumentNullException?
In this case, a null claimsPrincipal can return a null ID value and non-null claimsPrincipal returns non-null ID value.
return DeserializeFromJson<ClientInfo>(Base64UrlHelpers.DecodeToBytes(clientInfo)); | ||
} | ||
|
||
internal static T DeserializeFromJson<T>(byte[] jsonByteArray) |
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 keep DeserializeFromJson
. I think you can also leave the generic parameter and add where T : notnull
.
@@ -24,7 +24,7 @@ internal static void StoreTokenUsedToCallWebAPI(this HttpContext httpContext, Jw | |||
/// </summary> | |||
/// <param name="httpContext">Http context associated with the current request.</param> | |||
/// <returns><see cref="JwtSecurityToken"/> used to call the Web API.</returns> | |||
internal static JwtSecurityToken GetTokenUsedToCallWebAPI(this HttpContext httpContext) | |||
internal static JwtSecurityToken? GetTokenUsedToCallWebAPI(this HttpContext httpContext) |
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.
In this method (and similar ones), we expect httpContext
to have a value here. However, if null is passed in, we get an NPE. So we either need to return null or throw.
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.
For public APIs, we should always check for NULLs and throw NullArgumentException.
For internal APIs like this - use your best judgement - I think in most cases we can add a null check, but this is where you know best. If you can't decide, my advice would be throw, i.e. be stingy with returning NULLs.
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.e. be stingy with returning NULLs.
👍
@@ -15,19 +15,19 @@ internal class Metadata | |||
/// Preferred alias. | |||
/// </summary> | |||
[JsonProperty(PropertyName = "preferred_network")] | |||
public string PreferredNetwork { get; set; } | |||
public string? PreferredNetwork { get; set; } |
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 case is similar to Late-initialized properties, Data Transfer Objects and nullability. What is our intent - value is required for all these properties or optional?
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.
Very good point, I missed this 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.
Good thing about this work and PR is that it starts the discussions about the intent of our code. 👍
@bgavrilMS what's the status on this? just concerned the longer it sits the more out of sync it will get w/master. and the more work ultimately to address merge conflicts. |
Folks, I apologise but this is probably more work that I can handle for the next few weeks, especially as @pmaytak makes an excellent point about how to handle JSON + nullables, which requires more investment. |
Thanks @bgavrilMS ... @pmaytak is going to finish the work |
This is most of the work needed to conform to the nullable reference types. This makes the API more expressive, by showing which input / output types can be null or not.
https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references
This is NOT a breaking change.
Please read through my PR comments and decide if this is an avenue you wish to pursue. To sum up:
PROs:
CONs: