This is razor code but I think the same can happen in most any C# code in an event driven architecture.
private List<User> Users { get; set; }
protected override async Task OnInitializedAsync()
{
Users = await Context.Users.Include(u => u.Address).ToListAsync();
}
So the above code will initialize Users before it is ever accessed. However, it puts up a warning that a non-nullable variable is not being initialized.
Is this a case of assigning "default!" to it which is my way of saying don't worry it'll be initialized before it is accessed?
Update: This occurs inside a .razor page in the @code part. So it exists while the html is being rendered to pass back to the user's browser. I'm writing this code in an ASP.NET Core Blazor app.
The problem here is the Users object needs to be accessible to all the code in the .razor file. But the code is loaded in the async method. And this method is called as part of the .razor file creation, so it needs to be in there.
To elaborate on my comments, with too much pontification:
In OOP, regardless of language (be it Java, C#, C++, Swift, etc), the purpose of a
class's constructor is to initialize the object-instance's state from any parameters, and also to establish class invariants.The principle of class-invariants is more-often-than-lot (in my opinion) lost on so-many (perhaps even most?) users of C#, specifically, because of how the C# language (and its associated documentation and libraries) has evolved towards preferring default (parameterless) constructors, post-construction initialization, and mutable properties - all of which cannot be reconciled with the CS-theoretical, not practical, limits of static analysis which the C# compiler uses for
#nullablewarnings.class's constructor can make guarantees about the state of any of its instance fields (including auto-properties) - whereas (exceptingrequiredmembers in C# 11), a C# object-initializer is evaluated post-construction, is entirely optional, sets entirely arbitrary and non-exhaustive members.Therefore, to make use of
#nullableto the full-extent, one must simply get used to getting into the habit of writing parameterized constructors, despite their "expressive redundancy" in most cases (e.g. in a POCO DTO) having to repeat the class's members as ctor parameters and then assign each property in the ctor.recordtypes in C# 9 simplify this - butrecordtypes aren't as immutable as they seem: properties can still be overwritten with invalid values after the ctor has run in an object-initializer, which breaks the concept of class-invariants being enforced by the constructor - I'm really not happy with how that turned out in C# 9, grrr.I appreciate that this isn't possible in many cases, such as with Entity Framework and EF Core, which (as of early 2023) still doesn't support binding database query results to constructor parameters, only to property-setters - but people often are unaware that many other libraries/frameworks do support ctor binding, such as Newtonsoft.Json supports deserializing JSON objects to immutable C# objects via
[JsonConstructor]and attaching[JsonProperty]to each ctor parameter, for example.In other cases, namely UI/UX code, where your visual component/control/widget must inherit from some framework-provided base-class (e.g. WinForms'
System.Windows.Forms.Control, or WPF'sVisualorControl, or in Blazor:Microsoft.AspNetCore.Components.ComponentBase) - you'll find yourself with seemingly contradictory precepts: you can only "initialize" the Control's state /data in theOnLoad/OnInitializedAsyncmethod (not the constructor), but the C# compiler's#nullableanalysis recognizes that only the constructor can initialize class members and properly establish that certain fields will never benullat any point. It's a conundrum and the documentation and official examples and tutorials do often gloss this over (sometimes even with= null!, which I feel is just wrong).Taking Blazor's
ComponentBasefor example (as this is what the OP's code is targeting, after-all): immediately after when theComponentBasesubclass is created (i.e. the ctor runs), theSetParametersAsyncmethod is called, only after that then isOnInitializedAsynccalled - andOnInitializedAsynccan fail or need to beawaited, which means that the "laws" about constructors definitely still apply: any code consuming aComponentBasetype cannot necessarily depend any late initialization byOnInitializedAsyncto be guaranteed, especially not any error-handling code.List<User> Users { get; }property uninitialized (and thereforenull, despite the type not beingList<User>?) andOnInitializedAsync(which would set it to a non-nullvalue) were to fail, and then if thatComponentBasesubclass object were to be passed to some custom-error handling logic, then that error-handler itself would fail if it (rightfully, but incorrectly) assumed that theUsersproperty would never benull.Component-factory system whereby theOnInitialized{Async}logic could be moved into a factory-method or ctor with that all-or-nothing guarantee that we need for software we can reason about. But anyway...So given the above, there exist a few solutions:
INotifyPropertyChanged.List<T>as the collection-type for a property that will be used for data-binding: you're meant to useObservableCollection<T>, which solves the problem:ObservableCollection<T>is meant to be initialized only once by the constructor (thus satisfying the never-nullproblem), and is designed to be long-lived and mutable, so it's perfectly-fine to populate it inOnInitializedAsyncandOnParametersSet{Async}, which is how Blazor operates.Therefore, in my opinion, you should change your code to this:
Notice how, if the data-loading in
OnInitializeAsync, viaReloadUsersAsync, fails due to an exception being thrown from Entity Framework (which is common, e.g. SQL Server timeout, database down, etc) then the class-invariants ofUsersListComponent(i.e. that theUserscollection is nevernulland the property always exposes a single long-lived object-reference) always remain true, which means any code can safely consume yourUsersListComponentwithout risk of a dreaded unexpectedNullReferenceException.(And the
IsBusypart is just because it's an inevitable thing to add to any ViewModel/XAML-databound class that performs some IO).