csla icon indicating copy to clipboard operation
csla copied to clipboard

Use nullable reference types throughout CSLA codebase

Open JasonBock opened this issue 6 years ago • 44 comments

Is your feature request related to a problem? Please describe. C#8 adds nullable reference types. Not only will CSLA need to support this, but it should also annotate its members with not just ?, but with the new nullable attributes.

Describe the solution you'd like NRT support throughout CSLA.

Describe alternatives you've considered Nothing :)

Additional context Use this reference for details/information.

JasonBock avatar Aug 12 '19 14:08 JasonBock

@rockfordlhotka I'd love to tackle this issue. I don't like null :-) Btw the nullable annotation context is not a breaking change. The annotation is only a compiler feature during development and not something the runtime checks.

StefanOssendorf avatar Mar 17 '24 13:03 StefanOssendorf

I'd love to have you do this!

It'll be CSLA 9 thing, as I'm sure there'll be breaking changes.

rockfordlhotka avatar Mar 21 '24 19:03 rockfordlhotka

@rockfordlhotka I'll link you here from now on for questions regarding the nullable context :-) First question: https://github.com/MarimerLLC/csla/blob/579c84dd14398549c1b48be3892d543a1a956260/Source/Csla/Core/FieldManager/FieldData.cs#L28-L42

Is the empty ctor only there for the MobileFormatter? It's at least not used within CSLA itself. I'd love to mark it Obsolete(true) because it would remove a lot of "maybe nullable" things in the class itself. For example the name can than be guaranteed.

StefanOssendorf avatar May 06 '24 20:05 StefanOssendorf

MobileFormatter does require an empty default ctor. Whether that is explicit or implicit, it is required for all types that implement IMobileObject.

rockfordlhotka avatar May 07 '24 07:05 rockfordlhotka

In that case I'll mark it as Obsolete. MobileFormatter can still use it but it's not usable any more at compile time :-)

StefanOssendorf avatar May 09 '24 20:05 StefanOssendorf

And should I really add now everywhere a null argument check where it's currently just assumed to be not null? 😅🤔

StefanOssendorf avatar May 09 '24 20:05 StefanOssendorf

That seems wise doesn't it?

rockfordlhotka avatar May 10 '24 05:05 rockfordlhotka

I concur with @rockfordlhotka. If a parameter is a NRT, someone could still pass in null, so an ArgumentNullException.ThrowIfNull() call is warranted.

JasonBock avatar May 13 '24 12:05 JasonBock

Hey - this is now part of the permanent record!! @JasonBock agrees with me!! 😁

rockfordlhotka avatar May 13 '24 15:05 rockfordlhotka

@rockfordlhotka don't make me do it.... image

JasonBock avatar May 13 '24 15:05 JasonBock

@rockfordlhotka How should we handle null within the framework to e.g. reset/unset the ClientContext? How do we handle things like this: https://github.com/MarimerLLC/csla/blob/0f469350a0d72bf35d35deebb6660c6231c50b5d/Source/Csla/ApplicationContext.cs#L143-L147 I personally don't like to set something null when I just can assign a new instance to avoid that. At the cost of memory consumption obviously. But - and I think that's the biggest downside on allwoing null in such cases - we have to mark ClientContext as nullable which means every code says "Hey! This could be null here. Please check that" eventhough we know that won't ever be the case. But that's not expressable by the current annotations as far as I am aware.

StefanOssendorf avatar Jun 25 '24 13:06 StefanOssendorf

If a property can be null, it needs to be annotated as such. And yes, that means that every usage of that property needs to do a null check. If the context should never be null, it should not be annotated with ?, but, arguably, when you have a mutable property, you will need to do this regardless. The annotation is essentially a compilation helper, in that it can provide warnings to users to not do bad things with members that don't expect values to be null. But it doesn't stop it. You still have to check for null. And when the property is mutable, it means you always have to check it.

Consider this:

public sealed class Data
{
  public Data(string value)
  {
    ArgumentNullException.ThrowIfNull(value);
    this.Value = value;
  }

  public string Value { get; }
}

Since the property isn't settable after construction, there's no way it could be null. If it was, you would've received an exception during the creation of a Data instance. (Yes, you could do Reflection shenanigans to change that property value via the backing field, but I personally wouldn't be concerned with people digging themselves into holes this route). Therefore, I don't have to worry about checking Value all the time for a null value.

But if I do this:

public sealed class Data
{
  public Data(string value)
  {
    ArgumentNullException.ThrowIfNull(value);
    this.Value = value;
  }

  public string Value { get; set; }
}

Now I have to check for null every time I access Value. To keep the semantics of "Value will never be null", I would need to change the setter to do a null check, which would then ensure that Value wouldn't be null.

Relating this back to ClientContext, either you annotate it with ? because it's possible it could be null, or you don't, and then ensure that ClientContext can never be null. As you mentioned, this may mean you to provide an "empty" or "default" ClientContext instance. I don't know how that would work in CSLA. Or, since the current behavior is that it could be null, then put ? on it, and make it explicit for callers.

JasonBock avatar Jun 25 '24 13:06 JasonBock

Of course if a 3rd party can directly modifiy a property we have to make that nullable or change it to a backing field with a guard clause. In the case of ClientContext we have full control over it due to get/set methods where we can deny null as an acceptable value. The question is should we do that or not? I personally would say: Yes, we should. It would not only make our code more robust and clean but our users one, too.

StefanOssendorf avatar Jun 25 '24 13:06 StefanOssendorf

The current behavior states that null is a valid value, so it should be annotated with ?. However, I'm guessing this will be part of a breaking change release, so maybe the right thing to do is to not annotate it, and force that either a non-null reference is provided, or an exception will occur. We know what will happen if we leave it nullable, because that's the current state of CSLA. Making it non-nullable may be the "right" change. I'm guessing @rockfordlhotka should weigh in on this.

JasonBock avatar Jun 25 '24 13:06 JasonBock

@JasonBock Yes. I'm asking because we are working on a breaking change version. Otherwise you are perfectly right. I would have annotatd it and keep the null behavior. Since I'm adding it and we are in a breaking change version I'd like to make it a bit more nice imho :-)

StefanOssendorf avatar Jun 25 '24 13:06 StefanOssendorf

Thinking about it. The nullable feature would mean you can't use any auto properties because they don't adhere to the annotations. There are cases even in the asp.net core framework like HttpContext.Items which are not nullable but the framework does not prevent you from setting it null. So my stand will probably be: We use the feature so respect the feature. Otherwise garbage in -> garbage out :D

StefanOssendorf avatar Jun 25 '24 18:06 StefanOssendorf

The data portal has optimizations to minimize data on the wire through the use of null.

So if someone never uses a collection like ClientContext, then only a null token is serialized instead of the empty collection, which is much larger because it includes the type info.

As a result, anyone using a remote data portal is helped anytime a complex type has a null value instead of an instance of the type.

rockfordlhotka avatar Jun 25 '24 21:06 rockfordlhotka

When it comes to ClientContext (and LocalContext) the current implementation is to ensure that no one ever gets a null value, but behind the scenes the value might be null so we get the serialization optimization.

This means that the getter is what ensures that the value is never null. If the value is null, the setter creates it and returns the value.

That way, if someone doesn't use those context values (and this is a lot of users), then they pay no price for the feature existing in CSLA. But if someone does use the context dictionaries, then they are created on demand.

As a result, in my view, the public API shouldn't change - people shouldn't worry that the hidden/private value behind the scenes could be null. We can change the private implementation if desired - annotating the field with ?, and the getter keeps doing what it does today.

I don't see any good reason to break everyone's code with a public API change. Either they use the context dictionaries and the dictionaries get lazy created, or they don't use them and the private field behind the scenes remains null.

rockfordlhotka avatar Jun 26 '24 00:06 rockfordlhotka

The data portal has optimizations to minimize data on the wire through the use of null.

So if someone never uses a collection like ClientContext, then only a null token is serialized instead of the empty collection, which is much larger because it includes the type info.

As a result, anyone using a remote data portal is helped anytime a complex type has a null value instead of an instance of the type.

That's resonable. For me the correct place for such optimization would be the serializer and not the class to be serialized. But that's out of scope and maybe we'll tackle it at a later date.

StefanOssendorf avatar Jun 28 '24 08:06 StefanOssendorf

@rockfordlhotka Could you please take a look at Csla.DataPortalClient.DataPortalProxyDescriptor. This class is no where referenced. Neither by type or as a magic string. Otherwise we can remove it. And Csla.DataPortalClient.IApplicationContextProvider looks dead, too.

StefanOssendorf avatar Jul 30 '24 20:07 StefanOssendorf

Please also take a look at Csla.DataPortalClient.LocalProxyExtensions.UseLocalProxy(config, options). That implementation looks broken to me 🤔

StefanOssendorf avatar Jul 31 '24 15:07 StefanOssendorf

Please look at Csla.Reflect.DynamicMethodHandle ctor. The parameter parameters isn't used. Either that's a bug or it can be removed. For the latter I'll remove it :)

StefanOssendorf avatar Aug 03 '24 20:08 StefanOssendorf

DynamicMethodHandle

It appears in use to me?

        if (parameters == null)
        {
          inParams = [null];

        }
        else
        {
          inParams = parameters;
        }

rockfordlhotka avatar Aug 06 '24 01:08 rockfordlhotka

DynamicMethodHandle

It appears in use to me?

        if (parameters == null)
        {
          inParams = [null];

        }
        else
        {
          inParams = parameters;
        }

Yes. But inParams is not used. So when I remove inParams parameters is now unused, too.

StefanOssendorf avatar Aug 06 '24 03:08 StefanOssendorf

I think we need to create an issue to actually use the parameters. They are passed in when the ctor is called, and presumably are supposed to be used to ensure we use the right overload of the method.

rockfordlhotka avatar Aug 06 '24 18:08 rockfordlhotka

I'm now working on Csla.Rules.

StefanOssendorf avatar Aug 17 '24 12:08 StefanOssendorf

@rockfordlhotka I need some input please :) We have these two rules: MinValue<T> where T: IComparable https://github.com/MarimerLLC/csla/blob/aa14c3f86e3052d73cb06785b6fb6c381e6eb9f2/Source/Csla/Rules/CommonRules.cs#L371-L377 and MaxValue<T> where T: IComparable https://github.com/MarimerLLC/csla/blob/aa14c3f86e3052d73cb06785b6fb6c381e6eb9f2/Source/Csla/Rules/CommonRules.cs#L459-L465

What would you expect to happen, when value == null in the line of comparison? That's a possibility there. We can decide it counts as not violating the rule or we make the rule restriction tighter. For example T must be a struct.

StefanOssendorf avatar Aug 17 '24 17:08 StefanOssendorf

Another question: Any good reason why this lists can't just be initialized and hence empty if not neede and/or used? Makes the nullable stuff a lot easier and even more intuitive to use. Currently accessing DirtyProperties in a rule would return null instead of an empty list... https://github.com/MarimerLLC/csla/blob/aa14c3f86e3052d73cb06785b6fb6c381e6eb9f2/Source/Csla/Rules/RuleContext.cs#L84-L98 https://github.com/MarimerLLC/csla/blob/aa14c3f86e3052d73cb06785b6fb6c381e6eb9f2/Source/Csla/Rules/RuleContext.cs#L66-L82

StefanOssendorf avatar Aug 17 '24 18:08 StefanOssendorf

@rockfordlhotka I need some input please :) We have these two rules: MinValue<T> where T: IComparable

https://github.com/MarimerLLC/csla/blob/aa14c3f86e3052d73cb06785b6fb6c381e6eb9f2/Source/Csla/Rules/CommonRules.cs#L371-L377

and MaxValue<T> where T: IComparable https://github.com/MarimerLLC/csla/blob/aa14c3f86e3052d73cb06785b6fb6c381e6eb9f2/Source/Csla/Rules/CommonRules.cs#L459-L465

What would you expect to happen, when value == null in the line of comparison? That's a possibility there. We can decide it counts as not violating the rule or we make the rule restriction tighter. For example T must be a struct.

I think in this context we should consider null to be default(T) for the purpose of comparison.

rockfordlhotka avatar Aug 19 '24 19:08 rockfordlhotka

Another question: Any good reason why this lists can't just be initialized and hence empty if not neede and/or used? Makes the nullable stuff a lot easier and even more intuitive to use. Currently accessing DirtyProperties in a rule would return null instead of an empty list...

https://github.com/MarimerLLC/csla/blob/aa14c3f86e3052d73cb06785b6fb6c381e6eb9f2/Source/Csla/Rules/RuleContext.cs#L84-L98

https://github.com/MarimerLLC/csla/blob/aa14c3f86e3052d73cb06785b6fb6c381e6eb9f2/Source/Csla/Rules/RuleContext.cs#L66-L82

CSLA often doesn't initialize lists of types that might be serialized, because it is much smaller to serialize null than it is to serialize an empty list. This is because the serializer doesn't need to flow the list's type information for null, but it does need to flow the type information for an empty list so the empty list can be deserialized on the other end of the process.

rockfordlhotka avatar Aug 19 '24 19:08 rockfordlhotka