From 591c60c00a80e0108cf0ffa15b05c41cd803dced Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Mon, 13 Apr 2026 18:43:41 +0200 Subject: [PATCH 01/19] Add more nullable annotations --- .../Framework/Entity.INotifyDataErrorInfo.cs | 8 +-- .../Framework/EntityQuery.cs | 23 +++++---- .../Framework/EntityRef.cs | 38 +++++++------- .../Framework/IDomainClientFactory.cs | 2 + .../Framework/IEntityCollection.cs | 2 + .../Framework/IEntityCollection`1.cs | 6 ++- .../Framework/InvokeArgs.cs | 49 +++++-------------- .../Framework/LoadResult.cs | 2 + .../Framework/OperationBase.cs | 39 ++++++++------- 9 files changed, 79 insertions(+), 90 deletions(-) diff --git a/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs b/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs index 4c7e41423..607610a63 100644 --- a/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs +++ b/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs @@ -5,11 +5,13 @@ using System.ComponentModel.DataAnnotations; using System.Linq; +#nullable enable + namespace OpenRiaServices.Client { public abstract partial class Entity : INotifyDataErrorInfo { - private EventHandler _validationErrorsChanged; + private EventHandler? _validationErrorsChanged; /// /// Raises the event whenever validation errors have changed for a property. @@ -23,7 +25,7 @@ private void RaiseValidationErrorsChanged(string propertyName) /// /// Explicitly implement the event. /// - event EventHandler INotifyDataErrorInfo.ErrorsChanged + event EventHandler? INotifyDataErrorInfo.ErrorsChanged { add { this._validationErrorsChanged += value; } remove { this._validationErrorsChanged -= value; } @@ -42,7 +44,7 @@ event EventHandler INotifyDataErrorInfo.ErrorsChange /// or entity-level errors when is /// null or empty. /// - IEnumerable INotifyDataErrorInfo.GetErrors(string propertyName) + IEnumerable INotifyDataErrorInfo.GetErrors(string? propertyName) { IEnumerable results; diff --git a/src/OpenRiaServices.Client/Framework/EntityQuery.cs b/src/OpenRiaServices.Client/Framework/EntityQuery.cs index 98ca54268..ec175e192 100644 --- a/src/OpenRiaServices.Client/Framework/EntityQuery.cs +++ b/src/OpenRiaServices.Client/Framework/EntityQuery.cs @@ -1,12 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; + +#nullable enable namespace OpenRiaServices.Client { - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Linq.Expressions; - /// /// Class representing a query method invocation. LINQ query operators can /// also be applied to the query. @@ -15,11 +14,11 @@ public abstract class EntityQuery { private readonly string _queryName; private readonly Type _entityType; - private readonly IDictionary _parameters; + private readonly IDictionary? _parameters; private readonly bool _hasSideEffects; private readonly bool _isComposable; private bool _includeTotalCount; - private IQueryable _query; + private IQueryable? _query; private readonly DomainClient _domainClient; /// @@ -32,7 +31,7 @@ public abstract class EntityQuery /// if the method takes no parameters. /// True if the query has side-effects, false otherwise. /// True if the query supports composition, false otherwise. - internal EntityQuery(DomainClient domainClient, string queryName, Type entityType, IDictionary parameters, bool hasSideEffects, bool isComposable) + internal EntityQuery(DomainClient domainClient, string queryName, Type entityType, IDictionary? parameters, bool hasSideEffects, bool isComposable) { if (domainClient == null) { @@ -107,7 +106,7 @@ public string QueryName /// Optional parameters required by the query method. Returns null /// if the method takes no parameters. /// - public IDictionary Parameters + public IDictionary? Parameters { get { @@ -141,7 +140,7 @@ public bool IsComposable /// Gets the underlying for the query. Returns /// null if no query exists. /// - public IQueryable Query + public IQueryable? Query { get { @@ -196,11 +195,11 @@ internal EntityQuery(EntityQuery eq, IQueryable query) { } - internal new IQueryable Query + internal new IQueryable? Query { get { - return (IQueryable)base.Query; + return (IQueryable?)base.Query; } set { diff --git a/src/OpenRiaServices.Client/Framework/EntityRef.cs b/src/OpenRiaServices.Client/Framework/EntityRef.cs index 89076a4a5..0dbaa20d7 100644 --- a/src/OpenRiaServices.Client/Framework/EntityRef.cs +++ b/src/OpenRiaServices.Client/Framework/EntityRef.cs @@ -9,6 +9,8 @@ using System.Reflection; using OpenRiaServices.Client.Internal; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -19,9 +21,9 @@ public sealed class EntityRef : IEntityRef where TEntity : Entity { private readonly Entity _parent; private readonly MetaMember _metaMember; - private EntitySet _sourceSet; + private EntitySet? _sourceSet; private readonly Func _entityPredicate; - private TEntity _entity; + private TEntity? _entity; private bool _hasAssignedEntity; private bool _hasLoadedEntity; @@ -75,7 +77,7 @@ public EntityRef(Entity parent, string memberName, Func entityPre /// /// Gets or sets the associated /// - public TEntity Entity + public TEntity? Entity { get { @@ -143,13 +145,13 @@ public TEntity Entity { value.SetParent(this._parent, this.AssocAttribute); } - else + else // value == null => _entity is not null, since entityChanged == true { // when a composed entity is removed from its EntityRef, // its inferred as a delete - if (this._sourceSet != null && this._sourceSet.IsAttached(this._entity)) + if (this._sourceSet != null && this._sourceSet.IsAttached(this._entity!)) { - this._sourceSet.Remove(this._entity); + this._sourceSet.Remove(this._entity!); } } @@ -167,7 +169,7 @@ public TEntity Entity } } - private EntitySet SourceSet + private EntitySet? SourceSet { get { @@ -184,7 +186,7 @@ private EntitySet SourceSet /// All internal assignments should be made through this method. /// /// The new entity instance - private void SetValue(TEntity value) + private void SetValue(TEntity? value) { if (this._entity != value) { @@ -221,12 +223,12 @@ private bool Filter(TEntity entity) /// /// The collection of entities to search. /// The entity or null. - private TEntity GetSingleMatch(IEnumerable entities) + private TEntity? GetSingleMatch(IEnumerable entities) { IEnumerable enumerable = (entities as ICollection) ?? entities.OfType(); - TEntity entity = null; + TEntity? entity = null; foreach (TEntity currEntity in enumerable.Where(this.Filter)) { if (entity != null) @@ -282,12 +284,12 @@ private void MonitorEntitySet() /// /// The event sender. /// The collection changed event arguments. - private void SourceSet_CollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + private void SourceSet_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) { if (args.Action == NotifyCollectionChangedAction.Add && this._parent.EntityState != EntityState.New) { - TEntity newEntity = this.GetSingleMatch(args.NewItems); + TEntity? newEntity = this.GetSingleMatch(args.NewItems!); if ((newEntity != null) && (newEntity != this._entity)) { // if the referenced Entity has been added to the source EntitySet, @@ -297,7 +299,7 @@ private void SourceSet_CollectionChanged(object sender, NotifyCollectionChangedE } else if (args.Action == NotifyCollectionChangedAction.Remove) { - if (this._entity != null && args.OldItems.Contains(this._entity)) + if (this._entity != null && args.OldItems!.Contains(this._entity)) { // if the referenced Entity has been removed from the source EntitySet, // we need to clear out our cached reference and raise the notification @@ -331,7 +333,7 @@ private void OnEntityAssociationUpdated(Entity entity) return; } - TEntity typedEntity = entity as TEntity; + TEntity? typedEntity = entity as TEntity; if (typedEntity != null && (this._hasLoadedEntity || this._hasAssignedEntity)) { @@ -371,7 +373,7 @@ private void OnEntitySetChanged() /// /// The event sender. /// The property changed event arguments. - private void ParentEntityPropertyChanged(object sender, PropertyChangedEventArgs e) + private void ParentEntityPropertyChanged(object? sender, PropertyChangedEventArgs e) { // only need to reset if we have a cached value if (!this._hasLoadedEntity && !this._hasAssignedEntity) @@ -405,6 +407,7 @@ EntityAssociationAttribute IEntityRef.Association /// Gets a value indicating whether this EntityRef has been loaded or /// has had a value assigned to it. /// + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(Entity))] bool IEntityRef.HasValue { get @@ -413,7 +416,7 @@ bool IEntityRef.HasValue } } - Entity IEntityRef.Entity + Entity? IEntityRef.Entity { get { @@ -450,6 +453,7 @@ EntityAssociationAttribute Association /// Gets a value indicating whether this EntityRef has been loaded or /// has had a value assigned to it. /// + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(Entity))] bool HasValue { get; @@ -459,7 +463,7 @@ bool HasValue /// Gets the referenced entity loading it if it hasn't been loaded yet. /// To avoid the deferred load, inspect the HasValue property first. /// - Entity Entity + Entity? Entity { get; } diff --git a/src/OpenRiaServices.Client/Framework/IDomainClientFactory.cs b/src/OpenRiaServices.Client/Framework/IDomainClientFactory.cs index 5d5e0fc88..ce23251e5 100644 --- a/src/OpenRiaServices.Client/Framework/IDomainClientFactory.cs +++ b/src/OpenRiaServices.Client/Framework/IDomainClientFactory.cs @@ -1,6 +1,8 @@ using System; using System.Diagnostics.CodeAnalysis; +#nullable enable + namespace OpenRiaServices.Client { /// diff --git a/src/OpenRiaServices.Client/Framework/IEntityCollection.cs b/src/OpenRiaServices.Client/Framework/IEntityCollection.cs index f7e15681a..ea453347a 100644 --- a/src/OpenRiaServices.Client/Framework/IEntityCollection.cs +++ b/src/OpenRiaServices.Client/Framework/IEntityCollection.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +#nullable enable + namespace OpenRiaServices.Client { /// diff --git a/src/OpenRiaServices.Client/Framework/IEntityCollection`1.cs b/src/OpenRiaServices.Client/Framework/IEntityCollection`1.cs index cde774eec..4e408da3c 100644 --- a/src/OpenRiaServices.Client/Framework/IEntityCollection`1.cs +++ b/src/OpenRiaServices.Client/Framework/IEntityCollection`1.cs @@ -3,6 +3,8 @@ using System.Collections.Specialized; using System.ComponentModel; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -18,11 +20,11 @@ public interface IEntityCollection : ICollection, IReadOnlyCol /// /// Event raised whenever an is added to this collection /// - event EventHandler> EntityAdded; + event EventHandler>? EntityAdded; /// /// Event raised whenever an is removed from this collection /// - event EventHandler> EntityRemoved; + event EventHandler>? EntityRemoved; } } diff --git a/src/OpenRiaServices.Client/Framework/InvokeArgs.cs b/src/OpenRiaServices.Client/Framework/InvokeArgs.cs index ebf63c24d..453ca5333 100644 --- a/src/OpenRiaServices.Client/Framework/InvokeArgs.cs +++ b/src/OpenRiaServices.Client/Framework/InvokeArgs.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -8,11 +10,6 @@ namespace OpenRiaServices.Client /// public sealed class InvokeArgs { - private readonly string _operationName; - private readonly Type _returnType; - private readonly IDictionary _parameters; - private readonly bool _hasSideEffects; - /// /// Initializes a new instance of the InvokeArgs class /// @@ -21,7 +18,7 @@ public sealed class InvokeArgs /// Optional parameters to the operation. Specify null /// if the method takes no parameters. /// True if the operation has side-effects, false otherwise. - public InvokeArgs(string operationName, Type returnType, IDictionary parameters, bool hasSideEffects) + public InvokeArgs(string operationName, Type returnType, IDictionary? parameters, bool hasSideEffects) { if (string.IsNullOrEmpty(operationName)) { @@ -32,55 +29,31 @@ public InvokeArgs(string operationName, Type returnType, IDictionary /// Gets the name of the operation. /// - public string OperationName - { - get - { - return this._operationName; - } - } + public string OperationName { get; } /// /// Gets the return Type of the operation. /// - public Type ReturnType - { - get - { - return this._returnType; - } - } + public Type ReturnType { get; } /// /// Optional parameters required by the operation. Returns null /// if the method takes no parameters. /// - public IDictionary Parameters - { - get - { - return this._parameters; - } - } + public IDictionary? Parameters { get; } /// /// Gets a value indicating whether the operation has side-effects. /// - public bool HasSideEffects - { - get - { - return this._hasSideEffects; - } - } + public bool HasSideEffects { get; } } } diff --git a/src/OpenRiaServices.Client/Framework/LoadResult.cs b/src/OpenRiaServices.Client/Framework/LoadResult.cs index 5857454c5..f20bd6ed7 100644 --- a/src/OpenRiaServices.Client/Framework/LoadResult.cs +++ b/src/OpenRiaServices.Client/Framework/LoadResult.cs @@ -4,6 +4,8 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; +#nullable enable + namespace OpenRiaServices.Client { /// diff --git a/src/OpenRiaServices.Client/Framework/OperationBase.cs b/src/OpenRiaServices.Client/Framework/OperationBase.cs index 61ba28388..6d7043622 100644 --- a/src/OpenRiaServices.Client/Framework/OperationBase.cs +++ b/src/OpenRiaServices.Client/Framework/OperationBase.cs @@ -7,6 +7,8 @@ using System.Threading; using System.Threading.Tasks; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -14,15 +16,15 @@ namespace OpenRiaServices.Client /// public abstract class OperationBase : INotifyPropertyChanged { - private object _result; - private Exception _error; + private object? _result; + private Exception? _error; private bool _canceled; private bool _completed; - private readonly object _userState; - private PropertyChangedEventHandler _propChangedHandler; - private EventHandler _completedEventHandler; + private readonly object? _userState; + private PropertyChangedEventHandler? _propChangedHandler; + private EventHandler? _completedEventHandler; private bool _isErrorHandled; - private readonly CancellationTokenSource _cancellationTokenSource; + private readonly CancellationTokenSource? _cancellationTokenSource; private protected static TaskScheduler CurrentSynchronizationContextTaskScheduler => SynchronizationContext.Current != null ? TaskScheduler.FromCurrentSynchronizationContext() : TaskScheduler.Default; @@ -41,7 +43,7 @@ protected OperationBase(object userState, bool supportCancellation) /// /// Optional user state. /// cancellationTokenSource to use for cancellation - protected OperationBase(object userState, CancellationTokenSource cancellationTokenSource) + protected OperationBase(object userState, CancellationTokenSource? cancellationTokenSource) { this._userState = userState; this._cancellationTokenSource = cancellationTokenSource; @@ -68,12 +70,12 @@ public event EventHandler Completed } else { - this._completedEventHandler = (EventHandler)Delegate.Combine(this._completedEventHandler, value); + this._completedEventHandler = (EventHandler?)Delegate.Combine(this._completedEventHandler, value); } } remove { - this._completedEventHandler = (EventHandler)Delegate.Remove(this._completedEventHandler, value); + this._completedEventHandler = (EventHandler?)Delegate.Remove(this._completedEventHandler, value); } } @@ -124,12 +126,13 @@ public bool CanCancel /// /// Gets the operation error if the operation failed. /// - public Exception Error => this._error; + public Exception? Error => this._error; /// /// Gets a value indicating whether the operation has failed. If /// true, inspect the Error property for details. /// + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(Error))] public bool HasError => this._error != null; /// @@ -140,12 +143,12 @@ public bool CanCancel /// /// Gets the result of the async operation. /// - private protected object Result => this._result; + private protected object? Result => this._result; /// /// Gets the optional user state for this operation. /// - public object UserState => this._userState; + public object? UserState => this._userState; /// /// For an operation where is true, this method marks the error as handled. @@ -332,7 +335,7 @@ protected void SetError(Exception error) if (SynchronizationContext.Current is { } syncCtx) { // Capture exception and rethrow with original stack trace - syncCtx.Send(static (object state) => ((ExceptionDispatchInfo)state).Throw(), ExceptionDispatchInfo.Capture(error)); + syncCtx.Send(static (object? state) => ((ExceptionDispatchInfo)state!).Throw(), ExceptionDispatchInfo.Capture(error)); } else { @@ -383,9 +386,9 @@ private void InvokeCompleteCallbacks() catch (Exception ex) when (SynchronizationContext.Current is { } syncCtx) { // Capture exception and rethrow with original stack trace - syncCtx.Send(static (object state) => + syncCtx.Send(static (object? state) => { - ((ExceptionDispatchInfo)state).Throw(); + ((ExceptionDispatchInfo)state!).Throw(); }, ExceptionDispatchInfo.Capture(ex)); } } @@ -418,15 +421,15 @@ protected void RaisePropertyChanged(string propertyName) #region INotifyPropertyChanged Members - event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged + event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged { add { - this._propChangedHandler = (PropertyChangedEventHandler)Delegate.Combine(this._propChangedHandler, value); + this._propChangedHandler = (PropertyChangedEventHandler?)Delegate.Combine(this._propChangedHandler, value); } remove { - this._propChangedHandler = (PropertyChangedEventHandler)Delegate.Remove(this._propChangedHandler, value); + this._propChangedHandler = (PropertyChangedEventHandler?)Delegate.Remove(this._propChangedHandler, value); } } From 42a948a04b1223883978231527ff3b812bdb20f3 Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Mon, 13 Apr 2026 18:56:49 +0200 Subject: [PATCH 02/19] Add annotations --- .../Framework/InvokeCompletedResult.cs | 28 ++++--------- .../Framework/Polyfills.cs | 42 +++++++++++++++++++ .../Framework/SubmitResult.cs | 8 ++-- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/src/OpenRiaServices.Client/Framework/InvokeCompletedResult.cs b/src/OpenRiaServices.Client/Framework/InvokeCompletedResult.cs index 0182f62e2..52c3fbb6d 100644 --- a/src/OpenRiaServices.Client/Framework/InvokeCompletedResult.cs +++ b/src/OpenRiaServices.Client/Framework/InvokeCompletedResult.cs @@ -4,6 +4,8 @@ using System.ComponentModel.DataAnnotations; using System.Linq; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -11,14 +13,12 @@ namespace OpenRiaServices.Client /// public class InvokeCompletedResult { - private readonly object _returnValue; - private readonly ReadOnlyCollection _validationErrors; /// /// Initializes a new instance of the InvokeCompletedResult class /// /// The value returned from the server. - public InvokeCompletedResult(object returnValue) + public InvokeCompletedResult(object? returnValue) : this(returnValue, Array.Empty()) { } @@ -28,37 +28,25 @@ public InvokeCompletedResult(object returnValue) /// /// The value returned from the server. /// A collection of validation errors. - public InvokeCompletedResult(object returnValue, IEnumerable validationErrors) + public InvokeCompletedResult(object? returnValue, IEnumerable validationErrors) { if (validationErrors == null) { throw new ArgumentNullException(nameof(validationErrors)); } - this._returnValue = returnValue; - this._validationErrors = validationErrors.ToList().AsReadOnly(); + ReturnValue = returnValue; + ValidationErrors = validationErrors.ToList().AsReadOnly(); } /// /// Gets the validation errors. /// - public IReadOnlyCollection ValidationErrors - { - get - { - return this._validationErrors; - } - } + public IReadOnlyCollection ValidationErrors { get; } /// /// Gets the value returned from the server. /// - public object ReturnValue - { - get - { - return this._returnValue; - } - } + public object? ReturnValue { get; } } } diff --git a/src/OpenRiaServices.Client/Framework/Polyfills.cs b/src/OpenRiaServices.Client/Framework/Polyfills.cs index fb806ff0d..ad81d8d73 100644 --- a/src/OpenRiaServices.Client/Framework/Polyfills.cs +++ b/src/OpenRiaServices.Client/Framework/Polyfills.cs @@ -125,5 +125,47 @@ public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes member // specifies the type of dynamically accessed members. public DynamicallyAccessedMemberTypes MemberTypes { get; } } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated field or property member will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated field and property members will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } } #endif diff --git a/src/OpenRiaServices.Client/Framework/SubmitResult.cs b/src/OpenRiaServices.Client/Framework/SubmitResult.cs index 6cc84cc2c..65ce2c8d4 100644 --- a/src/OpenRiaServices.Client/Framework/SubmitResult.cs +++ b/src/OpenRiaServices.Client/Framework/SubmitResult.cs @@ -1,6 +1,8 @@ using System; using System.Linq; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -8,15 +10,13 @@ namespace OpenRiaServices.Client /// public class SubmitResult { - private readonly EntityChangeSet _changeSet; - /// /// Initializes a new instance of the class. /// /// The changeset which was submitted. public SubmitResult(EntityChangeSet changeSet) { - _changeSet = changeSet; + ChangeSet = changeSet; } /// @@ -25,6 +25,6 @@ public SubmitResult(EntityChangeSet changeSet) /// /// The changeset which was submitted. /// - public EntityChangeSet ChangeSet { get { return _changeSet; } } + public EntityChangeSet ChangeSet { get; } } } From 23acccaf04fb3938d56ecf3e4b9aa56f71111717 Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Mon, 13 Apr 2026 18:59:59 +0200 Subject: [PATCH 03/19] add nullability --- .../Framework/InvokeOperation.cs | 32 ++++++++++--------- .../Framework/OperationBase.cs | 6 ++-- .../Framework/SubmitOperation.cs | 9 ++++-- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/OpenRiaServices.Client/Framework/InvokeOperation.cs b/src/OpenRiaServices.Client/Framework/InvokeOperation.cs index 39d092ce8..8bab18f62 100644 --- a/src/OpenRiaServices.Client/Framework/InvokeOperation.cs +++ b/src/OpenRiaServices.Client/Framework/InvokeOperation.cs @@ -5,6 +5,8 @@ using System.Threading; using System.Threading.Tasks; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -12,9 +14,9 @@ namespace OpenRiaServices.Client /// public class InvokeOperation : OperationBase, IInvokeResult { - private IDictionary _parameters; - private IReadOnlyCollection _validationErrors; - private readonly Action _completeAction; + private IDictionary? _parameters; + private IReadOnlyCollection? _validationErrors; + private readonly Action? _completeAction; /// /// Initializes a new instance of the InvokeOperation class @@ -25,9 +27,9 @@ public class InvokeOperation : OperationBase, IInvokeResult /// Optional action to execute when the operation completes. /// Optional user state for the operation. /// which will be used to request cancellation if is called, if null then cancellation will not be possible - internal InvokeOperation(string operationName, IDictionary parameters, - Action completeAction, object userState, - CancellationTokenSource cancellationTokenSource) + internal InvokeOperation(string operationName, IDictionary? parameters, + Action? completeAction, object? userState, + CancellationTokenSource? cancellationTokenSource) : base(userState, cancellationTokenSource) { if (string.IsNullOrEmpty(operationName)) @@ -62,12 +64,12 @@ public IDictionary Parameters /// /// The for this operation. /// - private protected new IInvokeResult Result => (IInvokeResult)base.Result; + private protected new IInvokeResult? Result => (IInvokeResult?)base.Result; /// /// Gets the return value for the invoke operation. /// - public object Value => Result?.Value; + public object? Value => Result?.Value; /// /// Gets the validation errors. @@ -132,7 +134,7 @@ protected override void InvokeCompleteAction() /// The Type of the invoke return value. public sealed class InvokeOperation : InvokeOperation { - private readonly Action> _completeAction; + private readonly Action>? _completeAction; /// /// Initializes a new instance of the class. @@ -143,10 +145,10 @@ public sealed class InvokeOperation : InvokeOperation /// Optional user state for the operation. /// Task which, when completed, will Complete the operation and set either , cancelled or error /// which will be used to request cancellation if is called, if null then cancellation will not be possible - public InvokeOperation(string operationName, IDictionary parameters, - Action> completeAction, object userState, + public InvokeOperation(string operationName, IDictionary? parameters, + Action>? completeAction, object? userState, Task> invokeResultTask, - CancellationTokenSource cancellationTokenSource) + CancellationTokenSource? cancellationTokenSource) : base(operationName, parameters, /* completeAction */ null, /* userState */ userState, /* supportCancellation */ cancellationTokenSource) { this._completeAction = completeAction; @@ -157,7 +159,7 @@ public InvokeOperation(string operationName, IDictionary paramet { invokeResultTask.ContinueWith(static (loadTask, state) => { - ((InvokeOperation)state).CompleteTask(loadTask); + ((InvokeOperation)state!).CompleteTask(loadTask); } , (object)this , CancellationToken.None @@ -169,7 +171,7 @@ public InvokeOperation(string operationName, IDictionary paramet /// /// Gets the return value for the invoke operation. /// - public new TValue Value + public new TValue? Value { get { @@ -184,7 +186,7 @@ public InvokeOperation(string operationName, IDictionary paramet /// /// The for this operation. /// - private new InvokeResult Result => (InvokeResult)base.Result; + private new InvokeResult? Result => (InvokeResult?)base.Result; /// /// Invoke the completion callback. diff --git a/src/OpenRiaServices.Client/Framework/OperationBase.cs b/src/OpenRiaServices.Client/Framework/OperationBase.cs index 6d7043622..ae7e559cc 100644 --- a/src/OpenRiaServices.Client/Framework/OperationBase.cs +++ b/src/OpenRiaServices.Client/Framework/OperationBase.cs @@ -33,7 +33,7 @@ public abstract class OperationBase : INotifyPropertyChanged /// /// Optional user state. /// true to setup cancellationTokenSource and use that to handle cancellation - protected OperationBase(object userState, bool supportCancellation) + protected OperationBase(object? userState, bool supportCancellation) : this(userState, supportCancellation ? new CancellationTokenSource() : null) { } @@ -43,7 +43,7 @@ protected OperationBase(object userState, bool supportCancellation) /// /// Optional user state. /// cancellationTokenSource to use for cancellation - protected OperationBase(object userState, CancellationTokenSource? cancellationTokenSource) + protected OperationBase(object? userState, CancellationTokenSource? cancellationTokenSource) { this._userState = userState; this._cancellationTokenSource = cancellationTokenSource; @@ -227,7 +227,7 @@ protected internal void SetCancelled() /// Successfully completes the operation. /// /// The operation result. - protected void Complete(object result) + protected void Complete(object? result) { this.EnsureNotCompleted(); diff --git a/src/OpenRiaServices.Client/Framework/SubmitOperation.cs b/src/OpenRiaServices.Client/Framework/SubmitOperation.cs index d95e6e1da..d262ad969 100644 --- a/src/OpenRiaServices.Client/Framework/SubmitOperation.cs +++ b/src/OpenRiaServices.Client/Framework/SubmitOperation.cs @@ -4,6 +4,8 @@ using System.Threading; using System.Threading.Tasks; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -12,7 +14,7 @@ namespace OpenRiaServices.Client public sealed class SubmitOperation : OperationBase { private readonly EntityChangeSet _changeSet; - private readonly Action _completeAction; + private readonly Action? _completeAction; /// /// Initializes a new instance of the class. @@ -23,7 +25,7 @@ public sealed class SubmitOperation : OperationBase /// Task which, when completed, will Complete the operation and set result, cancelled or error /// which will be used to request cancellation if is called, if null then cancellation will not be possible public SubmitOperation(EntityChangeSet changeSet, - Action completeAction, object userState, + Action? completeAction, object? userState, Task sumitResultTask, CancellationTokenSource cancellationTokenSource) : base(userState, cancellationTokenSource) { @@ -31,6 +33,7 @@ public SubmitOperation(EntityChangeSet changeSet, { throw new ArgumentNullException(nameof(changeSet)); } + this._completeAction = completeAction; this._changeSet = changeSet; @@ -40,7 +43,7 @@ public SubmitOperation(EntityChangeSet changeSet, { sumitResultTask.ContinueWith(static (task, state) => { - var operation = (SubmitOperation)state; + var operation = (SubmitOperation)state!; operation.CompleteTask(task); } , (object)this From b02b02cee7e3d97c72194bbf470600dd98bedded Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Mon, 13 Apr 2026 19:09:02 +0200 Subject: [PATCH 04/19] update polyfills --- .../Framework/Polyfills.cs | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/OpenRiaServices.Client/Framework/Polyfills.cs b/src/OpenRiaServices.Client/Framework/Polyfills.cs index ad81d8d73..0017d79db 100644 --- a/src/OpenRiaServices.Client/Framework/Polyfills.cs +++ b/src/OpenRiaServices.Client/Framework/Polyfills.cs @@ -128,12 +128,7 @@ public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes member /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class MemberNotNullWhenAttribute : Attribute + internal sealed class MemberNotNullWhenAttribute : Attribute { /// Initializes the attribute with the specified return value condition and a field or property member. /// @@ -167,5 +162,25 @@ public MemberNotNullWhenAttribute(bool returnValue, params string[] members) /// Gets field or property member names. public string[] Members { get; } } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = [member]; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } } #endif From 3ce2042b9e19b6013cb33b9c62f864223faa3b4a Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Mon, 20 Apr 2026 18:32:29 +0200 Subject: [PATCH 05/19] Add nullable to a ferw server projects --- src/.editorconfig | 3 +- .../Framework/Polyfills.cs | 56 ++++++++++++++++++- .../Framework/Data/AuthorizationAttribute.cs | 27 +++++---- .../Framework/Data/AuthorizationContext.cs | 39 +++++++------ .../Framework/Data/DomainServiceContext.cs | 14 +++-- .../Framework/Data/DomainServiceErrorInfo.cs | 4 ++ .../Framework/Data/IDomainServiceFactory.cs | 2 + 7 files changed, 108 insertions(+), 37 deletions(-) diff --git a/src/.editorconfig b/src/.editorconfig index e660609cb..3a356d506 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -116,7 +116,8 @@ dotnet_diagnostic.VSTHRD010.severity = suggestion dotnet_diagnostic.CA1510.severity = none # CA1863: Cache a 'CompositeFormat' for repeated use in this formatting operation dotnet_diagnostic.CA1863.severity = none - +dotnet_diagnostic.CA1826.severity = warning +dotnet_code_quality.CA1826.exclude_ordefault_methods = true ############################### # C# Coding Conventions # diff --git a/src/OpenRiaServices.Client/Framework/Polyfills.cs b/src/OpenRiaServices.Client/Framework/Polyfills.cs index 0017d79db..07e32d888 100644 --- a/src/OpenRiaServices.Client/Framework/Polyfills.cs +++ b/src/OpenRiaServices.Client/Framework/Polyfills.cs @@ -1,12 +1,40 @@ #if !NET using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#nullable enable + +namespace System +{ + /// + /// Helper methods to allow "newer" .NET methods on older frameworks + /// + internal static class Polyfills + { + extension(ArgumentNullException) + { + public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (argument is null) + throw new ArgumentNullException(paramName); + } + } + + [DoesNotReturn] + private static void ThrowArgumentNullException(string? paramName) + { + throw new ArgumentNullException(paramName); + } + } +} namespace System.Collections.Generic { /// /// Helper methods to allow "newer" .NET methods on older frameworks /// - static class Polyfills + internal static class Polyfills { public static bool TryAdd(this IDictionary dictionary, TKey key, TValue value) { @@ -26,6 +54,20 @@ public static bool TryAdd(this IDictionary dictionar } } +namespace System.Runtime.CompilerServices +{ + [System.AttributeUsage(System.AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + internal sealed class CallerArgumentExpressionAttribute : Attribute + { + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + + public string ParameterName { get; private set; } + } +} + namespace System.Diagnostics.CodeAnalysis { // @@ -182,5 +224,17 @@ internal sealed class MemberNotNullAttribute : Attribute /// Gets field or property member names. public string[] Members { get; } } + + /// + /// Specifies that an output is not null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + /// + [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Parameter | System.AttributeTargets.Property | System.AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute { } + + /// + /// Specifies that a method will never return under any circumstance. + /// + [System.AttributeUsage(System.AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute { } } #endif diff --git a/src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs b/src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs index aec1b02b2..c5e8eea64 100644 --- a/src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs +++ b/src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs @@ -3,6 +3,8 @@ using System.Security.Principal; using DataAnnotationsResources = OpenRiaServices.Server.Resource; +#nullable enable + namespace System.ComponentModel.DataAnnotations { /// @@ -19,9 +21,9 @@ namespace System.ComponentModel.DataAnnotations /// public abstract class AuthorizationAttribute : Attribute { - private Func _errorMessageAccessor; - private string _errorMessage; - private Type _resourceType; + private Func? _errorMessageAccessor; + private string? _errorMessage; + private Type? _resourceType; /// /// Determines whether the given is authorized to perform a specific operation @@ -96,7 +98,7 @@ private Func ErrorMessageAccessor /// the mechanism that allows localization of error messages. If is null, this value /// is assumed to be a literal non-localized error message that can be used verbatim. /// - public string ErrorMessage + public string? ErrorMessage { get { @@ -116,7 +118,7 @@ public string ErrorMessage /// But if it is not null, is treated as the name of a static property within /// the specified that can be retrieved to yield the actual error message. /// - public Type ResourceType + public Type? ResourceType { get { @@ -153,7 +155,7 @@ protected string FormatErrorMessage(string operation) { // Create and cache an accessor for this. // We guarantee a non-null accessor or an InvalidOperationException if the properties are incorrect to create one. - string message = this.ErrorMessageAccessor(); + string? message = this.ErrorMessageAccessor(); // Optionally include the operation if the string contains {0} formatting information return string.Format(CultureInfo.CurrentCulture, message, operation); @@ -171,12 +173,13 @@ private Func CreateErrorMessageAccessor() if (this.ResourceType == null) { // An empty ErrorMessage returns a default message - if (string.IsNullOrEmpty(this.ErrorMessage)) + string? errorMessage = this.ErrorMessage; + if (string.IsNullOrEmpty(errorMessage)) { return () => DataAnnotationsResources.AuthorizationAttribute_Default_Message; } // Else returns the non-empty ErrorMessage - return () => this.ErrorMessage; + return () => errorMessage!; } // If there is a ResourceType, validate and return Reflection-based property getter @@ -198,14 +201,14 @@ private Func CreateErrorMessagePropertyAccessor() if (string.IsNullOrEmpty(this.ErrorMessage)) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, - DataAnnotationsResources.AuthorizationAttribute_Requires_ErrorMessage, this.GetType().FullName, this.ResourceType.FullName)); + DataAnnotationsResources.AuthorizationAttribute_Requires_ErrorMessage, this.GetType().FullName, this.ResourceType!.FullName)); } // Find the static public/internal property identified by ErrorMessage - PropertyInfo property = this.ResourceType.GetProperty(this.ErrorMessage, BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic); + PropertyInfo? property = this.ResourceType!.GetProperty(this.ErrorMessage, BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic); if (property != null) { - MethodInfo propertyGetter = property.GetGetMethod(true /*nonPublic*/); + MethodInfo? propertyGetter = property.GetGetMethod(true /*nonPublic*/); // We only support internal and public properties if (propertyGetter == null || (!propertyGetter.IsAssembly && !propertyGetter.IsPublic)) { @@ -226,7 +229,7 @@ private Func CreateErrorMessagePropertyAccessor() } // Looks good, give them a func that will eval dynamically in case it changes - return () => (string)property.GetValue(null, null); + return () => (string)property.GetValue(null, null)!; } #endregion } diff --git a/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs b/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs index 166f23346..073304b89 100644 --- a/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs +++ b/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Text; +#nullable enable + using DataAnnotationsResources = OpenRiaServices.Server.Resource; namespace System.ComponentModel.DataAnnotations @@ -34,12 +36,12 @@ namespace System.ComponentModel.DataAnnotations /// public sealed class AuthorizationContext : IServiceProvider, IDisposable { - private object _instance; - private string _operation; - private string _operationType; - private Dictionary _items; - private readonly IServiceProvider _parentServiceProvider; - private ServiceContainer _serviceContainer; + private object? _instance; + private string? _operation; + private string? _operationType; + private Dictionary? _items; + private readonly IServiceProvider? _parentServiceProvider; + private ServiceContainer? _serviceContainer; /// /// Initalizes a new instance of the class that can be used as a template. @@ -62,7 +64,7 @@ public sealed class AuthorizationContext : IServiceProvider, IDisposable /// /// Optional parent to which calls to /// can be delegated. - public AuthorizationContext(IServiceProvider serviceProvider) + public AuthorizationContext(IServiceProvider? serviceProvider) { this._parentServiceProvider = serviceProvider; } @@ -80,7 +82,7 @@ public AuthorizationContext(IServiceProvider serviceProvider) /// new dictionary, preventing consumers from modifying the original dictionary. /// /// When or is null or empty. - public AuthorizationContext(object instance, string operation, string operationType, IServiceProvider serviceProvider, IDictionary items) : this(serviceProvider) + public AuthorizationContext(object instance, string operation, string operationType, IServiceProvider? serviceProvider, IDictionary? items) : this(serviceProvider) { this.Setup(instance, operation, operationType, items); } @@ -144,7 +146,7 @@ public IServiceContainer ServiceContainer /// else is acceptable, they should allow the authorization request. /// /// - public object Instance + public object? Instance { get { @@ -159,13 +161,13 @@ public object Instance /// /// This property will never return null, but the dictionary may be empty. Changes made /// to items in this dictionary will never affect the original dictionary specified in the constructor. - public IDictionary Items + public IDictionary Items { get { if (this._items == null) { - this._items = new Dictionary(); + this._items = new Dictionary(); } return this._items; } @@ -197,7 +199,7 @@ public string OperationType { // This value is unconditionally off limits for a "template" AuthorizationContext this.EnsureNotTemplate(); - return this._operationType; + return this._operationType!; // _operation and _operationType are always set together, so we can be sure that if _operation is not null, then _operationType is not null either } } @@ -205,6 +207,9 @@ public string OperationType /// Helper method that throws if the current /// instance is only a template. /// +#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant + [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_operation))] +#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant private void EnsureNotTemplate() { if (this._operation == null) @@ -220,7 +225,7 @@ private void EnsureNotTemplate() /// Required operation name. /// Required operation type. /// Optional name/value pairs. - private void Setup(object instance, string operation, string operationType, IDictionary items) + private void Setup(object instance, string operation, string operationType, IDictionary? items) { // The instance will be null in situations such as query methods, so it is optional this._instance = instance; @@ -242,7 +247,7 @@ private void Setup(object instance, string operation, string operationType, IDic // Snapshot the dictionary if provided, else create lazily on demand if (items != null && items.Count != 0) { - this._items = new Dictionary(items); + this._items = new Dictionary(items); } } @@ -256,7 +261,7 @@ public void Dispose() // Developer remarks: this Dispose is adequate. We do not implement the full Dispose // pattern because this class is (a) sealed, and (b) has no finalizer. // This method is idempotent. - ServiceContainer serviceContainer = this._serviceContainer; + ServiceContainer? serviceContainer = this._serviceContainer; this._serviceContainer = null; if (serviceContainer != null) { @@ -275,7 +280,7 @@ public void Dispose() /// /// The type of the service needed. /// An instance of that service or null if it is not available. - public object GetService(Type serviceType) + public object? GetService(Type serviceType) { // Request for service container creates one if it does not already exist. if (serviceType == typeof(IServiceContainer)) @@ -287,7 +292,7 @@ public object GetService(Type serviceType) // asked for it in the past (which created one). By default, there is no service container. if (this._serviceContainer != null) { - object service = this._serviceContainer.GetService(serviceType); + object? service = this._serviceContainer.GetService(serviceType); if (service != null) { return service; diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs b/src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs index f4f3bc595..fca20e95c 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs @@ -5,6 +5,8 @@ using System.Security.Principal; using System.Threading; +#nullable enable + namespace OpenRiaServices.Server { /// @@ -21,7 +23,7 @@ public class DomainServiceContext : IServiceProvider /// The user calling the operation. /// The type of operation that is being executed. /// - public DomainServiceContext(IServiceProvider serviceProvider, IPrincipal user, DomainOperationType operationType, CancellationToken cancellationToken = default) + public DomainServiceContext(IServiceProvider serviceProvider, IPrincipal? user, DomainOperationType operationType, CancellationToken cancellationToken = default) { if (serviceProvider == null) { @@ -38,14 +40,14 @@ public DomainServiceContext(IServiceProvider serviceProvider, IPrincipal user, D /// [Obsolete("Use other constructor accepting a User, this will be removed in a future version")] public DomainServiceContext(IServiceProvider serviceProvider, DomainOperationType operationType) - : this(serviceProvider, (IPrincipal)serviceProvider.GetService(typeof(IPrincipal)), operationType) + : this(serviceProvider, (IPrincipal?)serviceProvider.GetService(typeof(IPrincipal)), operationType) { } /// /// Gets the operation that is being executed. /// - public DomainOperationEntry Operation { get; internal set; } + public DomainOperationEntry? Operation { get; internal set; } /// /// Gets the type of operation that is being executed. @@ -55,7 +57,7 @@ public DomainServiceContext(IServiceProvider serviceProvider, DomainOperationTyp /// /// The user for this context instance. /// - public IPrincipal User { get; } + public IPrincipal? User { get; } /// /// which may be used by hosting layer to request cancellation. @@ -71,9 +73,9 @@ public DomainServiceContext(IServiceProvider serviceProvider, DomainOperationTyp /// /// The type of the service needed. /// An instance of that service or null if it is not available. - public virtual object GetService(Type serviceType) + public virtual object? GetService(Type serviceType) { - return this._serviceProvider?.GetService(serviceType); + return this._serviceProvider.GetService(serviceType); } #endregion diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs b/src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs index 934550957..993d50553 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs @@ -1,5 +1,7 @@ using System; +#nullable enable + namespace OpenRiaServices.Server { /// @@ -16,6 +18,8 @@ public sealed class DomainServiceErrorInfo /// The exception that occurred. public DomainServiceErrorInfo(Exception exception) { + ArgumentNullException.ThrowIfNull(exception, nameof(exception)); + this._exception = exception; } diff --git a/src/OpenRiaServices.Server/Framework/Data/IDomainServiceFactory.cs b/src/OpenRiaServices.Server/Framework/Data/IDomainServiceFactory.cs index 9701bb4f4..beb0279eb 100644 --- a/src/OpenRiaServices.Server/Framework/Data/IDomainServiceFactory.cs +++ b/src/OpenRiaServices.Server/Framework/Data/IDomainServiceFactory.cs @@ -1,5 +1,7 @@ using System; +#nullable enable + namespace OpenRiaServices.Server { /// From 19ff5e84a1df8a783c030bee95dfdd8f8252acbe Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Mon, 20 Apr 2026 23:16:28 +0200 Subject: [PATCH 06/19] add more nullable annotations --- .../Framework/ValidationUtilities.cs | 4 +- .../Framework/Data/AuthorizationContext.cs | 6 +- .../Framework/Data/DomainOperationEntry.cs | 11 ++- .../Data/DomainOperationParameter.cs | 2 + .../Framework/Data/DomainService.cs | 97 +++++++++---------- 5 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs b/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs index 07b327a9e..a3ba15e25 100644 --- a/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs +++ b/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs @@ -23,13 +23,14 @@ namespace OpenRiaServices.Client { internal static class ValidationUtilities { +#nullable enable /// /// Creates a new for the current object instance. /// /// The object instance being validated. /// Optional context to inherit from. May be null. /// A new validation context. - internal static ValidationContext CreateValidationContext(object instance, ValidationContext parentContext) + internal static ValidationContext CreateValidationContext(object instance, ValidationContext? parentContext) { if (instance == null) { @@ -39,6 +40,7 @@ internal static ValidationContext CreateValidationContext(object instance, Valid ValidationContext context = new ValidationContext(instance, parentContext, parentContext != null ? parentContext.Items : null); return context; } +#nullable restore /// /// Internal helper method for getting a method from an object instance that matches diff --git a/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs b/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs index 073304b89..0086d0d4e 100644 --- a/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs +++ b/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs @@ -82,7 +82,7 @@ public AuthorizationContext(IServiceProvider? serviceProvider) /// new dictionary, preventing consumers from modifying the original dictionary. /// /// When or is null or empty. - public AuthorizationContext(object instance, string operation, string operationType, IServiceProvider? serviceProvider, IDictionary? items) : this(serviceProvider) + public AuthorizationContext(object? instance, string operation, string operationType, IServiceProvider? serviceProvider, IDictionary? items) : this(serviceProvider) { this.Setup(instance, operation, operationType, items); } @@ -100,7 +100,7 @@ public AuthorizationContext(object instance, string operation, string operationT /// An existing to use as a template. /// When or is null or empty /// or is null. - public AuthorizationContext(object instance, string operation, string operationType, AuthorizationContext authorizationContext) + public AuthorizationContext(object? instance, string operation, string operationType, AuthorizationContext authorizationContext) : this((IServiceProvider) authorizationContext) { if (authorizationContext == null) @@ -225,7 +225,7 @@ private void EnsureNotTemplate() /// Required operation name. /// Required operation type. /// Optional name/value pairs. - private void Setup(object instance, string operation, string operationType, IDictionary? items) + private void Setup(object? instance, string operation, string operationType, IDictionary? items) { // The instance will be null in situations such as query methods, so it is optional this._instance = instance; diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs b/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs index 181101016..b1b809ccd 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs @@ -9,6 +9,8 @@ using System.Threading; using System.Threading.Tasks; +#nullable enable + namespace OpenRiaServices.Server { /// @@ -17,9 +19,9 @@ namespace OpenRiaServices.Server public abstract class DomainOperationEntry { private readonly string _methodName; - private Attribute _operationAttribute; + private Attribute? _operationAttribute; private AttributeCollection _attributes; - private Type _associatedType; + private Type? _associatedType; private readonly Type _actualReturnType; private readonly Type _domainServiceType; private LazyBool _requiresValidation; @@ -232,6 +234,7 @@ internal bool RequiresAuthorization /// Based on the operation type specified, create the default corresponding attribute /// if it hasn't been specified explicitly, and add it to the attributes collection. /// + [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_operationAttribute))] private void InitializeOperationAttribute() { if (this._operationAttribute != null) @@ -294,7 +297,7 @@ private void InitializeOperationAttribute() } break; default: - break; + throw new NotImplementedException(Operation.ToString()); } if (attributeCreated) @@ -365,7 +368,7 @@ internal set /// this will be the element type of the return type (or the singleton return Type), /// and for all other methods this will be the Type of the first method parameter. /// - public Type AssociatedType + public Type? AssociatedType { get { diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainOperationParameter.cs b/src/OpenRiaServices.Server/Framework/Data/DomainOperationParameter.cs index 5a8443ff1..4734611d4 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainOperationParameter.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainOperationParameter.cs @@ -3,6 +3,8 @@ using System.Globalization; using System.Linq; +#nullable enable + namespace OpenRiaServices.Server { /// diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs index a7c1b9cad..b240c8a23 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs @@ -11,6 +11,8 @@ using System.Threading.Tasks; using DataAnnotationsResources = OpenRiaServices.Server.Resource; +#nullable enable + namespace OpenRiaServices.Server { /// @@ -24,12 +26,12 @@ public abstract class DomainService : IDisposable internal const int TotalCountUndefined = -1; private const int TotalCountEqualsResultSetCount = -2; - private static IDomainServiceFactory s_domainServiceFactory; + private static IDomainServiceFactory? s_domainServiceFactory; - private ChangeSet _changeSet; - private DomainServiceContext _serviceContext; - private DomainServiceDescription _serviceDescription; - private ValidationContext _validationContext; + private ChangeSet? _changeSet; + private DomainServiceContext? _serviceContext; + private DomainServiceDescription? _serviceDescription; + private ValidationContext? _validationContext; #endregion // Fields /// @@ -68,16 +70,7 @@ public static IDomainServiceFactory Factory /// Gets the for this . /// protected DomainServiceDescription ServiceDescription - { - get - { - if (this._serviceDescription == null) - { - this._serviceDescription = DomainServiceDescription.GetDescription(this.GetType()); - } - return this._serviceDescription; - } - } + => this._serviceDescription ??= DomainServiceDescription.GetDescription(this.GetType()); /// /// Gets the active for this . @@ -102,7 +95,7 @@ public DomainServiceContext ServiceContext /// making these services and items available to each /// involved in validation. /// - protected ValidationContext ValidationContext + protected ValidationContext? ValidationContext { get { return this._validationContext; } set { this._validationContext = value; } @@ -137,7 +130,7 @@ protected ValidationContext ValidationContext /// value set in this property must be disposed explicitly by the developer. /// /// - protected AuthorizationContext AuthorizationContext { get; set; } + protected AuthorizationContext? AuthorizationContext { get; set; } /// /// Requests authorization for the given . @@ -151,7 +144,7 @@ protected ValidationContext ValidationContext /// The results of authorization. indicates the /// authorization request is allowed. Any other value indicates it was denied. /// - public AuthorizationResult IsAuthorized(DomainOperationEntry domainOperationEntry, object entity) + public AuthorizationResult IsAuthorized(DomainOperationEntry domainOperationEntry, object? entity) { if (domainOperationEntry == null) { @@ -179,8 +172,8 @@ public AuthorizationResult IsAuthorized(DomainOperationEntry domainOperationEntr string operationType = domainOperationEntry.OperationType; // Formulate an AuthorizationContext from the optional template provided by the user - AuthorizationContext contextTemplate = this.AuthorizationContext; - AuthorizationResult result = null; + AuthorizationContext? contextTemplate = this.AuthorizationContext; + AuthorizationResult? result = null; // If the developer specified a template, we will clone from it and use it as the IServiceProvider. // If the user did not, we create a new instance and use the ServiceContext as the IServiceProvider. @@ -193,7 +186,7 @@ public AuthorizationResult IsAuthorized(DomainOperationEntry domainOperationEntr context.Items[typeof(Type)] = domainOperationEntry.AssociatedType; // The principal is retrieved through the DomainServiceContext as a service - IPrincipal principal = this.ServiceContext != null ? this.ServiceContext.User : null; + IPrincipal? principal = this.ServiceContext != null ? this.ServiceContext.User : null; // Null principal is denied before going any further -- it is contractually required for the // authorization attributes. @@ -228,7 +221,7 @@ public void Dispose() /// /// Gets the current . Returns null if no change operations are being performed. /// - protected ChangeSet ChangeSet + protected ChangeSet? ChangeSet { get { @@ -270,9 +263,9 @@ public virtual void Initialize(DomainServiceContext context) /// The query results. May be null if there are no query results. public async virtual ValueTask> QueryAsync(QueryDescription queryDescription, CancellationToken cancellationToken) { - IEnumerable enumerableResult = null; - IReadOnlyCollection enumeratedResult = null; - List validationErrorList = null; + IEnumerable? enumerableResult = null; + IReadOnlyCollection? enumeratedResult = null; + List? validationErrorList = null; int totalCount; try @@ -299,7 +292,7 @@ public async virtual ValueTask> QueryAsync(QueryDescrip return new ServiceQueryResult(validationErrorList); } - object result = null; + object? result = null; this.ServiceContext.Operation = queryDescription.Method; try @@ -589,11 +582,11 @@ private static AuthorizationResult EvaluateAuthorization(IEnumerableThe for the operation being validated. /// An optional to use for services and items, or null. /// true if all the operations in the specified list are valid. - internal static bool ValidateOperations(IEnumerable operations, DomainServiceDescription domainServiceDescription, ValidationContext validationContextRoot) + internal static bool ValidateOperations(IEnumerable operations, DomainServiceDescription domainServiceDescription, ValidationContext? validationContextRoot) { bool success = true; IEnumerable operationsToValidate = operations.Where( @@ -665,16 +658,17 @@ internal static bool ValidateOperations(IEnumerable operations, // if the entity has a custom method invocation, validate the all method calls if (hasCustomMethod) { - Dictionary invokedActions = null; - for (int i = 0; i < operation.EntityActions.Count; ++i) + Dictionary? invokedActions = null; + for (int i = 0; i < operation.EntityActions!.Count; ++i) { var action = operation.EntityActions[i]; - DomainOperationEntry customMethodOperation = null; + DomainOperationEntry? customMethodOperation = null!; bool alreadyInvoked = invokedActions != null && invokedActions.TryGetValue(action.Key, out customMethodOperation); - if (!alreadyInvoked) // equivalent to (customMethodOperation != null) - customMethodOperation = domainServiceDescription.GetCustomMethod(operation.Entity.GetType(), action.Key); + + // ResolveOperations has already validated that the custom method exists, so we know this won't return null + customMethodOperation ??= domainServiceDescription.GetCustomMethod(operation.Entity.GetType(), action.Key)!; // Of more that one action is queued check for multiple invocations, this way we don't have to allocate // a dictionary unless necessary @@ -744,7 +738,7 @@ protected virtual void Dispose(bool disposing) /// True if the is authorized, false otherwise. protected virtual bool AuthorizeChangeSet() { - foreach (ChangeSetEntry op in this.ChangeSet.ChangeSetEntries) + foreach (ChangeSetEntry op in this.ChangeSet!.ChangeSetEntries) { object entity = op.Entity; @@ -777,7 +771,7 @@ protected virtual bool AuthorizeChangeSet() protected virtual ValueTask ValidateChangeSetAsync(CancellationToken cancellationToken) { // Perform validation on the each of the operations. - var result = ValidateOperations(this.ChangeSet.ChangeSetEntries, this.ServiceDescription, this.ValidationContext); + var result = ValidateOperations(this.ChangeSet!.ChangeSetEntries, this.ServiceDescription, this.ValidationContext); return new ValueTask(result); } @@ -792,7 +786,7 @@ await this.InvokeCudOperationsAsync() await this.InvokeCustomOperations() .ConfigureAwait(false); - return !this.ChangeSet.HasError; + return !this.ChangeSet!.HasError; } /// @@ -820,11 +814,11 @@ protected virtual void OnError(DomainServiceErrorInfo errorInfo) { } private void ResolveOperations() { // Resolve and set the DomainOperationEntry for each operation in the changeset - foreach (ChangeSetEntry changeSetEntry in this.ChangeSet.ChangeSetEntries) + foreach (ChangeSetEntry changeSetEntry in this.ChangeSet!.ChangeSetEntries) { // resolve the DomainOperationEntry Type entityType = changeSetEntry.Entity.GetType(); - DomainOperationEntry domainOperationEntry = null; + DomainOperationEntry? domainOperationEntry = null; if (changeSetEntry.Operation == DomainOperation.Insert || changeSetEntry.Operation == DomainOperation.Update || changeSetEntry.Operation == DomainOperation.Delete) @@ -1010,12 +1004,12 @@ private async Task PersistChangeSetAsyncInternal(CancellationToken cancell if (e.Value != null && e.ValidationResult != null) { IEnumerable updateOperations = - this.ChangeSet.ChangeSetEntries.Where( + this.ChangeSet!.ChangeSetEntries.Where( p => p.Operation == DomainOperation.Insert || p.Operation == DomainOperation.Update || p.Operation == DomainOperation.Delete); - ChangeSetEntry operation = updateOperations.SingleOrDefault(p => object.ReferenceEquals(p.Entity, e.Value)); + ChangeSetEntry? operation = updateOperations.FirstOrDefault(p => object.ReferenceEquals(p.Entity, e.Value)); if (operation != null) { ValidationResultInfo error = new ValidationResultInfo(e.ValidationResult.ErrorMessage, e.ValidationResult.MemberNames); @@ -1029,13 +1023,14 @@ private async Task PersistChangeSetAsyncInternal(CancellationToken cancell } } - return !this.ChangeSet.HasError; + return !this.ChangeSet!.HasError; } /// /// Ensures the has been initialized properly. /// /// if this service instance hasn't been initialized. + [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_serviceContext))] private void EnsureInitialized() { if (this._serviceContext == null) @@ -1050,7 +1045,7 @@ private void EnsureInitialized() /// /// The to validate. /// The optional entity instance being authorized. - private void ValidateMethodPermissions(DomainOperationEntry domainOperationEntry, object entity) + private void ValidateMethodPermissions(DomainOperationEntry domainOperationEntry, object? entity) { AuthorizationResult result = this.IsAuthorized(domainOperationEntry, entity); if (result != AuthorizationResult.Allowed) @@ -1065,7 +1060,7 @@ private void ValidateMethodPermissions(DomainOperationEntry domainOperationEntry private async Task InvokeCudOperationsAsync() { object[] parameters = new object[1]; - foreach (ChangeSetEntry operation in this.ChangeSet.ChangeSetEntries + foreach (ChangeSetEntry operation in this.ChangeSet!.ChangeSetEntries .Where(op => op.Operation == DomainOperation.Insert || op.Operation == DomainOperation.Update || op.Operation == DomainOperation.Delete)) @@ -1107,7 +1102,7 @@ await this.InvokeDomainOperationEntryAsync(operation.DomainOperationEntry, param /// private async Task InvokeCustomOperations() { - foreach (ChangeSetEntry operation in this.ChangeSet.ChangeSetEntries.Where(op => op.EntityActions != null && op.EntityActions.Any())) + foreach (ChangeSetEntry operation in this.ChangeSet!.ChangeSetEntries.Where(op => op.EntityActions != null && op.EntityActions.Any())) { foreach (var entityAction in operation.EntityActions) { @@ -1138,11 +1133,11 @@ await this.InvokeDomainOperationEntryAsync(customMethodOperation, parameters, op /// The parameters to invoke domain operation entry with. /// The object associated with the domain operation entry for logging errors (if any). /// The result of the . - private async ValueTask InvokeDomainOperationEntryAsync(DomainOperationEntry domainOperationEntry, object[] parameters, ChangeSetEntry operation) + private async ValueTask InvokeDomainOperationEntryAsync(DomainOperationEntry domainOperationEntry, object[] parameters, ChangeSetEntry operation) { // invoke the domain operation entry and catch continuable errors if any this.ServiceContext.Operation = domainOperationEntry; - string stackTrace = null; + string? stackTrace = null; try { try @@ -1187,7 +1182,7 @@ private async ValueTask InvokeDomainOperationEntryAsync(DomainOperationE this.ServiceContext.Operation = null; } - return new ValueTask((object)null); + return new ValueTask((object?)null); } /// @@ -1318,10 +1313,10 @@ public DomainService CreateDomainService(Type domainServiceType, DomainServiceCo nameof(domainServiceType)); } - DomainService domainService = null; + DomainService? domainService = null; try { - domainService = (DomainService)Activator.CreateInstance(domainServiceType); + domainService = (DomainService)Activator.CreateInstance(domainServiceType)!; } catch (TargetInvocationException ex) { From 8dab21193f56c41a2c2a542481c25d7caaaa5f20 Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Mon, 20 Apr 2026 23:17:07 +0200 Subject: [PATCH 07/19] add test retry --- src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/UpdateTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/UpdateTests.cs b/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/UpdateTests.cs index 5701803e4..b1a8646d8 100644 --- a/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/UpdateTests.cs +++ b/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/UpdateTests.cs @@ -2312,6 +2312,7 @@ public virtual void ResolveTest_ReturnTrueNoResolve() [TestMethod] [Asynchronous] [Microsoft.VisualStudio.TestTools.UnitTesting.Description("Resolve method returns false and does not remove any conflicts")] + [Retry(3)] public virtual void ResolveTest_ReturnFalse() { Northwind nw1 = new Northwind(this.ServiceUri); From 88bc8727289130c954bf5a062973cf8e435bbb40 Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Tue, 21 Apr 2026 07:15:14 +0200 Subject: [PATCH 08/19] fix operation nullable --- .../Framework/LoadOperation.cs | 28 ++++++++++--------- .../Framework/SubmitOperation.cs | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/OpenRiaServices.Client/Framework/LoadOperation.cs b/src/OpenRiaServices.Client/Framework/LoadOperation.cs index 30c3a98df..894ef0cce 100644 --- a/src/OpenRiaServices.Client/Framework/LoadOperation.cs +++ b/src/OpenRiaServices.Client/Framework/LoadOperation.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using OpenRiaServices.Client.Data; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -16,10 +18,10 @@ namespace OpenRiaServices.Client /// public abstract class LoadOperation : OperationBase, ILoadResult { - private ReadOnlyObservableLoaderCollection _entities; - private ReadOnlyObservableLoaderCollection _allEntities; + private ReadOnlyObservableLoaderCollection? _entities; + private ReadOnlyObservableLoaderCollection? _allEntities; - private IReadOnlyCollection _validationErrors; + private IReadOnlyCollection? _validationErrors; private readonly LoadBehavior _loadBehavior; private readonly EntityQuery _query; @@ -30,7 +32,7 @@ public abstract class LoadOperation : OperationBase, ILoadResult /// to use for the load operation. /// Optional user state for the operation. /// non null to enable to be cancelled when is called - private protected LoadOperation(EntityQuery query, LoadBehavior loadBehavior, object userState, CancellationTokenSource cancellationTokenSource) + private protected LoadOperation(EntityQuery query, LoadBehavior loadBehavior, object? userState, CancellationTokenSource? cancellationTokenSource) : base(userState, cancellationTokenSource) { if (query == null) @@ -44,7 +46,7 @@ private protected LoadOperation(EntityQuery query, LoadBehavior loadBehavior, ob /// /// The for this operation. /// - private protected new ILoadResult Result => (ILoadResult)base.Result; + private protected new ILoadResult? Result => (ILoadResult?)base.Result; /// /// The for this load operation. @@ -164,8 +166,8 @@ private protected static ReadOnlyObservableLoaderCollection AsReadOnlyO public sealed class LoadOperation : LoadOperation where TEntity : Entity { - private ReadOnlyObservableLoaderCollection _entities; - private readonly Action> _completeAction; + private ReadOnlyObservableLoaderCollection? _entities; + private readonly Action>? _completeAction; /// /// Initializes a new instance of the class. @@ -176,7 +178,7 @@ public sealed class LoadOperation : LoadOperation /// Optional user state for the operation. /// true to enable to be cancelled when is called internal LoadOperation(EntityQuery query, LoadBehavior loadBehavior, - Action> completeAction, object userState, + Action>? completeAction, object? userState, bool supportCancellation) : base(query, loadBehavior, userState, supportCancellation ? new CancellationTokenSource() : null) { @@ -194,10 +196,10 @@ internal LoadOperation(EntityQuery query, LoadBehavior loadBehavior, /// which will be used to request cancellation if is called, if null then cancellation will not be possible public LoadOperation(EntityQuery query, LoadBehavior loadBehavior, - Action> completeAction, - object userState, + Action>? completeAction, + object? userState, Task> loadResultTask, - CancellationTokenSource cancellationTokenSource) + CancellationTokenSource? cancellationTokenSource) : base(query, loadBehavior, userState, cancellationTokenSource) { if (loadResultTask == null) @@ -213,7 +215,7 @@ public LoadOperation(EntityQuery query, { loadResultTask.ContinueWith(static (task, state) => { - ((LoadOperation)state).CompleteTask(task); + ((LoadOperation)state!).CompleteTask(task); } , (object)this , CancellationToken.None @@ -270,7 +272,7 @@ protected override void InvokeCompleteAction() /// /// The for this operation. /// - private new LoadResult Result => (LoadResult)base.Result; + private new LoadResult? Result => (LoadResult?)base.Result; /// /// Successfully completes the load operation with the specified result. diff --git a/src/OpenRiaServices.Client/Framework/SubmitOperation.cs b/src/OpenRiaServices.Client/Framework/SubmitOperation.cs index d262ad969..aa777737e 100644 --- a/src/OpenRiaServices.Client/Framework/SubmitOperation.cs +++ b/src/OpenRiaServices.Client/Framework/SubmitOperation.cs @@ -26,7 +26,7 @@ public sealed class SubmitOperation : OperationBase /// which will be used to request cancellation if is called, if null then cancellation will not be possible public SubmitOperation(EntityChangeSet changeSet, Action? completeAction, object? userState, - Task sumitResultTask, CancellationTokenSource cancellationTokenSource) + Task sumitResultTask, CancellationTokenSource? cancellationTokenSource) : base(userState, cancellationTokenSource) { if (changeSet == null) From 9b77ebe53feda2a87ba8f406f88e6983dfd78f3a Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Tue, 21 Apr 2026 07:24:57 +0200 Subject: [PATCH 09/19] Remove MemberNotNull which was not correct --- .../Framework/Data/DomainOperationEntry.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs b/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs index b1b809ccd..693939f0e 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs @@ -234,7 +234,6 @@ internal bool RequiresAuthorization /// Based on the operation type specified, create the default corresponding attribute /// if it hasn't been specified explicitly, and add it to the attributes collection. /// - [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_operationAttribute))] private void InitializeOperationAttribute() { if (this._operationAttribute != null) @@ -297,18 +296,19 @@ private void InitializeOperationAttribute() } break; default: - throw new NotImplementedException(Operation.ToString()); + // We get here during ReflectionDomainServiceDescriptionProvider.ClassifyDomainOperation before DomainOperation is set + break; } if (attributeCreated) { if (this._attributes == null) { - this._attributes = new AttributeCollection(this._operationAttribute); + this._attributes = new AttributeCollection(this._operationAttribute!); } else { - this._attributes = AttributeCollection.FromExisting(this._attributes, this._operationAttribute); + this._attributes = AttributeCollection.FromExisting(this._attributes, this._operationAttribute!); } } } From ecd3a1f891105f842f8f5a12f7590a7b33e51daf Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Tue, 21 Apr 2026 16:11:34 +0200 Subject: [PATCH 10/19] fix review comments --- Changelog.md | 7 ++++++- src/OpenRiaServices.Client/Framework/EntityQuery.cs | 2 +- src/OpenRiaServices.Client/Framework/Polyfills.cs | 10 +++++++++- .../Framework/Data/AuthorizationAttribute.cs | 2 +- .../Framework/Data/DomainService.cs | 5 ++--- .../Framework/Data/DomainServiceErrorInfo.cs | 7 ++----- 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Changelog.md b/Changelog.md index 9e6bd1db8..b33332d7a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,9 +15,14 @@ This release gives some ❤️ to AvaloniaUI and other frameworks which use ILis * Add `IReadOnlyList` interface to `EntityCollection` and `EntitySet` and `LoadResult` * Add `IList` interface to `EntityCollection` * Nullability annotations for `EntitySet` and `EntityCollection` +* Nullability annotations for a large part of the most used public API + +### Server +* Added Nullability annotations for a few core types of public API **Other** -* Fixed build pipeline problems after updating to VS 2016 +* Fixed build pipeline problems after updating to VS 2026 +* Improved polyfills to allow modernization of the codebase * Analyzer fixes * Fix CA1860: Prefer comparing 'Count' to 0 rather than using 'Any()' * Fix MSTEST****: varius test related diff --git a/src/OpenRiaServices.Client/Framework/EntityQuery.cs b/src/OpenRiaServices.Client/Framework/EntityQuery.cs index ec175e192..93e3f6186 100644 --- a/src/OpenRiaServices.Client/Framework/EntityQuery.cs +++ b/src/OpenRiaServices.Client/Framework/EntityQuery.cs @@ -31,7 +31,7 @@ public abstract class EntityQuery /// if the method takes no parameters. /// True if the query has side-effects, false otherwise. /// True if the query supports composition, false otherwise. - internal EntityQuery(DomainClient domainClient, string queryName, Type entityType, IDictionary? parameters, bool hasSideEffects, bool isComposable) + private protected EntityQuery(DomainClient domainClient, string queryName, Type entityType, IDictionary? parameters, bool hasSideEffects, bool isComposable) { if (domainClient == null) { diff --git a/src/OpenRiaServices.Client/Framework/Polyfills.cs b/src/OpenRiaServices.Client/Framework/Polyfills.cs index 07e32d888..5bf000a4d 100644 --- a/src/OpenRiaServices.Client/Framework/Polyfills.cs +++ b/src/OpenRiaServices.Client/Framework/Polyfills.cs @@ -17,8 +17,16 @@ internal static class Polyfills public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) { if (argument is null) - throw new ArgumentNullException(paramName); + ThrowArgumentNullException(paramName); } + +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. + public static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (string.IsNullOrEmpty(argument)) + ThrowArgumentNullException(paramName); + } +#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. } [DoesNotReturn] diff --git a/src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs b/src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs index c5e8eea64..632c98521 100644 --- a/src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs +++ b/src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs @@ -155,7 +155,7 @@ protected string FormatErrorMessage(string operation) { // Create and cache an accessor for this. // We guarantee a non-null accessor or an InvalidOperationException if the properties are incorrect to create one. - string? message = this.ErrorMessageAccessor(); + string message = this.ErrorMessageAccessor(); // Optionally include the operation if the string contains {0} formatting information return string.Format(CultureInfo.CurrentCulture, message, operation); diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs index b240c8a23..9f2e4f99d 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs @@ -663,12 +663,11 @@ internal static bool ValidateOperations(IEnumerable operations, { var action = operation.EntityActions[i]; - DomainOperationEntry? customMethodOperation = null!; + DomainOperationEntry? customMethodOperation = null; bool alreadyInvoked = invokedActions != null && invokedActions.TryGetValue(action.Key, out customMethodOperation); - // ResolveOperations has already validated that the custom method exists, so we know this won't return null - customMethodOperation ??= domainServiceDescription.GetCustomMethod(operation.Entity.GetType(), action.Key)!; + customMethodOperation ??= domainServiceDescription.GetCustomMethod(operation.Entity.GetType(), action.Key); // Of more that one action is queued check for multiple invocations, this way we don't have to allocate // a dictionary unless necessary diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs b/src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs index 993d50553..b91bf6cd8 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs @@ -18,7 +18,7 @@ public sealed class DomainServiceErrorInfo /// The exception that occurred. public DomainServiceErrorInfo(Exception exception) { - ArgumentNullException.ThrowIfNull(exception, nameof(exception)); + ArgumentNullException.ThrowIfNull(exception); this._exception = exception; } @@ -34,10 +34,7 @@ public Exception Error } set { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } + ArgumentNullException.ThrowIfNull(value); this._exception = value; } From eead70c2bc7f8d86897f364bd1c872d671c7d05c Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Tue, 21 Apr 2026 16:36:27 +0200 Subject: [PATCH 11/19] Fix nullability comments --- .../Framework/Entity.INotifyDataErrorInfo.cs | 2 +- src/OpenRiaServices.Client/Framework/EntityQuery.cs | 8 ++++---- src/OpenRiaServices.Client/Framework/EntityRef.cs | 2 -- src/OpenRiaServices.Client/Framework/InvokeArgs.cs | 4 ++-- src/OpenRiaServices.Client/Framework/SubmitResult.cs | 2 ++ .../Serialization/DataContractRequestSerializer.cs | 2 +- .../Framework/Data/AuthorizationContext.cs | 9 ++++++--- .../Framework/Data/DomainOperationEntry.cs | 10 +++++----- .../Framework/Data/DomainService.cs | 2 +- .../Framework/Data/DomainServiceContext.cs | 12 +++++------- .../Framework/Data/ServiceInvokeResult.cs | 9 ++++++--- 11 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs b/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs index 607610a63..d029279ac 100644 --- a/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs +++ b/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs @@ -53,7 +53,7 @@ IEnumerable INotifyDataErrorInfo.GetErrors(string? propertyName) // If the property name is null or empty, then we want to include errors // where the member names array is empty, or where the member names array // contains a null or empty string. - results = this.ValidationErrors.Where(e => !e.MemberNames.Any() || e.MemberNames.Contains(propertyName)); + results = this.ValidationErrors.Where(e => !e.MemberNames.Any() || e.MemberNames.Any(string.IsNullOrEmpty)); } else { diff --git a/src/OpenRiaServices.Client/Framework/EntityQuery.cs b/src/OpenRiaServices.Client/Framework/EntityQuery.cs index 93e3f6186..4387584b4 100644 --- a/src/OpenRiaServices.Client/Framework/EntityQuery.cs +++ b/src/OpenRiaServices.Client/Framework/EntityQuery.cs @@ -14,7 +14,7 @@ public abstract class EntityQuery { private readonly string _queryName; private readonly Type _entityType; - private readonly IDictionary? _parameters; + private readonly IDictionary? _parameters; private readonly bool _hasSideEffects; private readonly bool _isComposable; private bool _includeTotalCount; @@ -31,7 +31,7 @@ public abstract class EntityQuery /// if the method takes no parameters. /// True if the query has side-effects, false otherwise. /// True if the query supports composition, false otherwise. - private protected EntityQuery(DomainClient domainClient, string queryName, Type entityType, IDictionary? parameters, bool hasSideEffects, bool isComposable) + private protected EntityQuery(DomainClient domainClient, string queryName, Type entityType, IDictionary? parameters, bool hasSideEffects, bool isComposable) { if (domainClient == null) { @@ -106,7 +106,7 @@ public string QueryName /// Optional parameters required by the query method. Returns null /// if the method takes no parameters. /// - public IDictionary? Parameters + public IDictionary? Parameters { get { @@ -185,7 +185,7 @@ public bool IncludeTotalCount /// The entity type. public sealed class EntityQuery : EntityQuery where TEntity : Entity { - internal EntityQuery(DomainClient domainClient, string queryName, IDictionary parameters, bool hasSideEffects, bool isComposable) + internal EntityQuery(DomainClient domainClient, string queryName, IDictionary parameters, bool hasSideEffects, bool isComposable) : base(domainClient, queryName, typeof(TEntity), parameters, hasSideEffects, isComposable) { } diff --git a/src/OpenRiaServices.Client/Framework/EntityRef.cs b/src/OpenRiaServices.Client/Framework/EntityRef.cs index 0dbaa20d7..048da2dbc 100644 --- a/src/OpenRiaServices.Client/Framework/EntityRef.cs +++ b/src/OpenRiaServices.Client/Framework/EntityRef.cs @@ -407,7 +407,6 @@ EntityAssociationAttribute IEntityRef.Association /// Gets a value indicating whether this EntityRef has been loaded or /// has had a value assigned to it. /// - [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(Entity))] bool IEntityRef.HasValue { get @@ -453,7 +452,6 @@ EntityAssociationAttribute Association /// Gets a value indicating whether this EntityRef has been loaded or /// has had a value assigned to it. /// - [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(Entity))] bool HasValue { get; diff --git a/src/OpenRiaServices.Client/Framework/InvokeArgs.cs b/src/OpenRiaServices.Client/Framework/InvokeArgs.cs index 453ca5333..b5e80afdf 100644 --- a/src/OpenRiaServices.Client/Framework/InvokeArgs.cs +++ b/src/OpenRiaServices.Client/Framework/InvokeArgs.cs @@ -18,7 +18,7 @@ public sealed class InvokeArgs /// Optional parameters to the operation. Specify null /// if the method takes no parameters. /// True if the operation has side-effects, false otherwise. - public InvokeArgs(string operationName, Type returnType, IDictionary? parameters, bool hasSideEffects) + public InvokeArgs(string operationName, Type returnType, IDictionary? parameters, bool hasSideEffects) { if (string.IsNullOrEmpty(operationName)) { @@ -49,7 +49,7 @@ public InvokeArgs(string operationName, Type returnType, IDictionary - public IDictionary? Parameters { get; } + public IDictionary? Parameters { get; } /// /// Gets a value indicating whether the operation has side-effects. diff --git a/src/OpenRiaServices.Client/Framework/SubmitResult.cs b/src/OpenRiaServices.Client/Framework/SubmitResult.cs index 65ce2c8d4..bdd88c280 100644 --- a/src/OpenRiaServices.Client/Framework/SubmitResult.cs +++ b/src/OpenRiaServices.Client/Framework/SubmitResult.cs @@ -16,6 +16,8 @@ public class SubmitResult /// The changeset which was submitted. public SubmitResult(EntityChangeSet changeSet) { + ArgumentNullException.ThrowIfNull(changeSet); + ChangeSet = changeSet; } diff --git a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractRequestSerializer.cs b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractRequestSerializer.cs index f308aab83..534fe7fb7 100644 --- a/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractRequestSerializer.cs +++ b/src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractRequestSerializer.cs @@ -61,7 +61,7 @@ protected DataContractRequestSerializer(DomainOperationEntry operation, DataCont { Type actualReturnType = operation.Operation switch { - DomainOperation.Query => typeof(QueryResult<>).MakeGenericType(operation.AssociatedType), + DomainOperation.Query => typeof(QueryResult<>).MakeGenericType(operation.AssociatedType!), DomainOperation.Invoke => operation.ReturnType, DomainOperation.Custom when operation.Name == "Submit" => typeof(IEnumerable), _ => throw new NotSupportedException() diff --git a/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs b/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs index 0086d0d4e..26b514e35 100644 --- a/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs +++ b/src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs @@ -199,7 +199,7 @@ public string OperationType { // This value is unconditionally off limits for a "template" AuthorizationContext this.EnsureNotTemplate(); - return this._operationType!; // _operation and _operationType are always set together, so we can be sure that if _operation is not null, then _operationType is not null either + return this._operationType; } } @@ -208,15 +208,18 @@ public string OperationType /// instance is only a template. /// #pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant - [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_operation))] -#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant +#pragma warning disable CS8774 // _operation and _operationType are always set together, so we can be sure that if _operation is not null, then _operationType is not null either + [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_operation), nameof(_operationType))] private void EnsureNotTemplate() + { if (this._operation == null) { throw new InvalidOperationException(DataAnnotationsResources.AuthorizationContext_Template_Only); } } +#pragma warning restore CS8774 // Member must have a non-null value when exiting. +#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant /// /// Helper method to initialize some ctor parameters diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs b/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs index 693939f0e..cb8567df1 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs @@ -356,7 +356,7 @@ internal set /// The parameters to pass to the method. /// A cancellation token that can be used to signal cancellation of this operation /// The return value of the invoked method. - public abstract ValueTask InvokeAsync(DomainService domainService, object[] parameters, CancellationToken cancellationToken); + public abstract ValueTask InvokeAsync(DomainService domainService, object[] parameters, CancellationToken cancellationToken); /// /// Gets the type of domain operation implemented by the method. @@ -410,7 +410,7 @@ public Type? AssociatedType /// The total number of rows for the input query without any paging applied to it. /// A cancellation token that can be used to signal cancellation of this operation /// The return value of the invoked method. - internal ValueTask InvokeAsync(DomainService domainService, object[] parameters, out int totalCount, CancellationToken cancellationToken) + internal ValueTask InvokeAsync(DomainService domainService, object[] parameters, out int totalCount, CancellationToken cancellationToken) { if (this.HasOutCountParameter) { @@ -418,13 +418,13 @@ internal ValueTask InvokeAsync(DomainService domainService, object[] par parameters.CopyTo(parametersWithCount, 0); parametersWithCount[parameters.Length] = 0; - ValueTask invokeTask = this.InvokeAsync(domainService, parametersWithCount, cancellationToken); + ValueTask invokeTask = this.InvokeAsync(domainService, parametersWithCount, cancellationToken); // Cant use await since method has out parameter so we need to block - object result = invokeTask.IsCompleted ? invokeTask.GetAwaiter().GetResult() : invokeTask.AsTask().GetAwaiter().GetResult(); + object? result = invokeTask.IsCompleted ? invokeTask.GetAwaiter().GetResult() : invokeTask.AsTask().GetAwaiter().GetResult(); totalCount = (int)parametersWithCount[parameters.Length]; - return new ValueTask(result); + return new ValueTask(result); } else { diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs index 9f2e4f99d..5e6c66a5a 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs @@ -442,7 +442,7 @@ public virtual async ValueTask InvokeAsync(InvokeDescriptio this.ServiceContext.Operation = invokeDescription.Method; try { - object returnValue = await invokeDescription.Method.InvokeAsync(this, invokeDescription.ParameterValues, cancellationToken) + object? returnValue = await invokeDescription.Method.InvokeAsync(this, invokeDescription.ParameterValues, cancellationToken) .ConfigureAwait(false); return new ServiceInvokeResult(returnValue); diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs b/src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs index fca20e95c..fd2e7d0f1 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs @@ -23,12 +23,10 @@ public class DomainServiceContext : IServiceProvider /// The user calling the operation. /// The type of operation that is being executed. /// - public DomainServiceContext(IServiceProvider serviceProvider, IPrincipal? user, DomainOperationType operationType, CancellationToken cancellationToken = default) + public DomainServiceContext(IServiceProvider serviceProvider, IPrincipal user, DomainOperationType operationType, CancellationToken cancellationToken = default) { - if (serviceProvider == null) - { - throw new ArgumentNullException(nameof(serviceProvider)); - } + ArgumentNullException.ThrowIfNull(serviceProvider); + this._serviceProvider = serviceProvider; this.OperationType = operationType; this.User = user; @@ -40,7 +38,7 @@ public DomainServiceContext(IServiceProvider serviceProvider, IPrincipal? user, /// [Obsolete("Use other constructor accepting a User, this will be removed in a future version")] public DomainServiceContext(IServiceProvider serviceProvider, DomainOperationType operationType) - : this(serviceProvider, (IPrincipal?)serviceProvider.GetService(typeof(IPrincipal)), operationType) + : this(serviceProvider, (IPrincipal)serviceProvider.GetService(typeof(IPrincipal))!, operationType) { } @@ -57,7 +55,7 @@ public DomainServiceContext(IServiceProvider serviceProvider, DomainOperationTyp /// /// The user for this context instance. /// - public IPrincipal? User { get; } + public IPrincipal User { get; } /// /// which may be used by hosting layer to request cancellation. diff --git a/src/OpenRiaServices.Server/Framework/Data/ServiceInvokeResult.cs b/src/OpenRiaServices.Server/Framework/Data/ServiceInvokeResult.cs index 651876005..8d4ec40fb 100644 --- a/src/OpenRiaServices.Server/Framework/Data/ServiceInvokeResult.cs +++ b/src/OpenRiaServices.Server/Framework/Data/ServiceInvokeResult.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +#nullable enable + namespace OpenRiaServices.Server { /// @@ -15,7 +17,7 @@ public struct ServiceInvokeResult /// Create a with the result of an successfully invoked Invoke operation /// /// the result from the invoked operation (or null if it was a void method) - public ServiceInvokeResult(object result) + public ServiceInvokeResult(object? result) { Result = result; ValidationErrors = null; @@ -39,17 +41,18 @@ public ServiceInvokeResult(IReadOnlyCollection validationError /// /// The result of the invocation, or null if is true /// - public object Result { get; } + public object? Result { get; } /// /// true if the query unsuccessfull and contains the errors. /// false means that the query was successfull with results in /// + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(ValidationErrors))] public bool HasValidationErrors => ValidationErrors != null; /// /// Gets the validation erros if any, or null if there were no validation errors. /// - public IReadOnlyCollection ValidationErrors { get; } + public IReadOnlyCollection? ValidationErrors { get; } } } From 295f60bb208210d153c77584c9859eb27ee1e501 Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Tue, 21 Apr 2026 22:30:28 +0200 Subject: [PATCH 12/19] nullable fixes --- Changelog.md | 6 ++++-- src/OpenRiaServices.Client/Framework/EntityQuery.cs | 3 ++- .../Framework/InvokeOperation.cs | 10 +++++----- src/OpenRiaServices.Client/Framework/InvokeResult.cs | 12 +++++++----- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Changelog.md b/Changelog.md index b33332d7a..9606ade2e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,12 +15,14 @@ This release gives some ❤️ to AvaloniaUI and other frameworks which use ILis * Add `IReadOnlyList` interface to `EntityCollection` and `EntitySet` and `LoadResult` * Add `IList` interface to `EntityCollection` * Nullability annotations for `EntitySet` and `EntityCollection` -* Nullability annotations for a large part of the most used public API +* Nullability annotations for many parts of the most used public API ### Server + * Added Nullability annotations for a few core types of public API -**Other** +### Other + * Fixed build pipeline problems after updating to VS 2026 * Improved polyfills to allow modernization of the codebase * Analyzer fixes diff --git a/src/OpenRiaServices.Client/Framework/EntityQuery.cs b/src/OpenRiaServices.Client/Framework/EntityQuery.cs index 4387584b4..12acc9e50 100644 --- a/src/OpenRiaServices.Client/Framework/EntityQuery.cs +++ b/src/OpenRiaServices.Client/Framework/EntityQuery.cs @@ -185,7 +185,7 @@ public bool IncludeTotalCount /// The entity type. public sealed class EntityQuery : EntityQuery where TEntity : Entity { - internal EntityQuery(DomainClient domainClient, string queryName, IDictionary parameters, bool hasSideEffects, bool isComposable) + internal EntityQuery(DomainClient domainClient, string queryName, IDictionary? parameters, bool hasSideEffects, bool isComposable) : base(domainClient, queryName, typeof(TEntity), parameters, hasSideEffects, isComposable) { } @@ -195,6 +195,7 @@ internal EntityQuery(EntityQuery eq, IQueryable query) { } + /// internal new IQueryable? Query { get diff --git a/src/OpenRiaServices.Client/Framework/InvokeOperation.cs b/src/OpenRiaServices.Client/Framework/InvokeOperation.cs index 8bab18f62..c0991443d 100644 --- a/src/OpenRiaServices.Client/Framework/InvokeOperation.cs +++ b/src/OpenRiaServices.Client/Framework/InvokeOperation.cs @@ -14,7 +14,7 @@ namespace OpenRiaServices.Client /// public class InvokeOperation : OperationBase, IInvokeResult { - private IDictionary? _parameters; + private IDictionary? _parameters; private IReadOnlyCollection? _validationErrors; private readonly Action? _completeAction; @@ -27,7 +27,7 @@ public class InvokeOperation : OperationBase, IInvokeResult /// Optional action to execute when the operation completes. /// Optional user state for the operation. /// which will be used to request cancellation if is called, if null then cancellation will not be possible - internal InvokeOperation(string operationName, IDictionary? parameters, + internal InvokeOperation(string operationName, IDictionary? parameters, Action? completeAction, object? userState, CancellationTokenSource? cancellationTokenSource) : base(userState, cancellationTokenSource) @@ -49,13 +49,13 @@ internal InvokeOperation(string operationName, IDictionary? para /// /// Gets the collection of parameters to the operation. /// - public IDictionary Parameters + public IDictionary Parameters { get { if (this._parameters == null) { - this._parameters = new Dictionary(); + this._parameters = new Dictionary(); } return this._parameters; } @@ -145,7 +145,7 @@ public sealed class InvokeOperation : InvokeOperation /// Optional user state for the operation. /// Task which, when completed, will Complete the operation and set either , cancelled or error /// which will be used to request cancellation if is called, if null then cancellation will not be possible - public InvokeOperation(string operationName, IDictionary? parameters, + public InvokeOperation(string operationName, IDictionary? parameters, Action>? completeAction, object? userState, Task> invokeResultTask, CancellationTokenSource? cancellationTokenSource) diff --git a/src/OpenRiaServices.Client/Framework/InvokeResult.cs b/src/OpenRiaServices.Client/Framework/InvokeResult.cs index 587b0d446..c17cd8a4c 100644 --- a/src/OpenRiaServices.Client/Framework/InvokeResult.cs +++ b/src/OpenRiaServices.Client/Framework/InvokeResult.cs @@ -1,6 +1,8 @@ using System; using System.Linq; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -9,7 +11,7 @@ namespace OpenRiaServices.Client /// interface IInvokeResult { - object Value { get; } + object? Value { get; } } /// @@ -17,7 +19,7 @@ interface IInvokeResult /// public class InvokeResult : IInvokeResult { - object IInvokeResult.Value => null; + object? IInvokeResult.Value => null; } /// @@ -42,16 +44,16 @@ public InvokeResult(T value) /// /// The value. /// - public T Value => _value; + public T? Value => _value; - object IInvokeResult.Value => _value; + object? IInvokeResult.Value => _value; /// /// Implicit conversion to so that does not need to be used /// in most cases. /// /// - public static implicit operator T(InvokeResult invokeResult) + public static implicit operator T?(InvokeResult invokeResult) { return invokeResult.Value; } From e0664fe318118de1feb834734b0fd22adb4ea743 Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Tue, 21 Apr 2026 22:31:08 +0200 Subject: [PATCH 13/19] add missing xml comments to EntityExtensions --- src/OpenRiaServices.Client/Framework/Entity.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/OpenRiaServices.Client/Framework/Entity.cs b/src/OpenRiaServices.Client/Framework/Entity.cs index b733632ec..71309dc3c 100644 --- a/src/OpenRiaServices.Client/Framework/Entity.cs +++ b/src/OpenRiaServices.Client/Framework/Entity.cs @@ -1954,37 +1954,49 @@ namespace OpenRiaServices.Client.EntityExtensions { public static class EntityExtensions { + /// public static IDictionary ExtractState(this Entity targetEntity) { return targetEntity.ExtractState(); } + + /// public static void ExtractState(this Entity targetEntity, IDictionary entityStateToApply) { targetEntity.ApplyState(entityStateToApply); } + + /// public static void ExtractState(this Entity targetEntity, IDictionary entityStateToApply, LoadBehavior loadBehavior) { targetEntity.ApplyState(entityStateToApply, loadBehavior); } + /// public static void UpdateOriginalValues(this Entity targetEntity, Entity entity) { targetEntity.UpdateOriginalValues(entity); } + + /// public static void UpdateOriginalValues(this Entity targetEntity, IDictionary entityStateToApply) { targetEntity.UpdateOriginalValues(entityStateToApply); } + + /// public static void Merge(this Entity targetEntity, Entity otherEntity, LoadBehavior loadBehavior) { targetEntity.Merge(otherEntity, loadBehavior); } + /// public static void Merge(this Entity targetEntity, IDictionary otherState, LoadBehavior loadBehavior) { targetEntity.Merge(otherState, loadBehavior); } + /// public static EntitySet GetEntitySet(this TEntity entity) where TEntity : Entity { return entity.EntitySet as EntitySet; From 31cf5b4845e08f9d97b77aceb1e747205a7b55cd Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Tue, 21 Apr 2026 22:37:33 +0200 Subject: [PATCH 14/19] Update src/OpenRiaServices.Client/Framework/EntityQuery.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/OpenRiaServices.Client/Framework/EntityQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenRiaServices.Client/Framework/EntityQuery.cs b/src/OpenRiaServices.Client/Framework/EntityQuery.cs index 12acc9e50..368985605 100644 --- a/src/OpenRiaServices.Client/Framework/EntityQuery.cs +++ b/src/OpenRiaServices.Client/Framework/EntityQuery.cs @@ -195,7 +195,7 @@ internal EntityQuery(EntityQuery eq, IQueryable query) { } - /// + /// internal new IQueryable? Query { get From 0f0f4d0c0344c9f01d0bc55f8664438d76fcab61 Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Tue, 21 Apr 2026 22:43:36 +0200 Subject: [PATCH 15/19] Apply suggestion from @coderabbitai[bot] Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/OpenRiaServices.Server/Framework/Data/DomainService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs index 5e6c66a5a..016251127 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs @@ -186,7 +186,7 @@ public AuthorizationResult IsAuthorized(DomainOperationEntry domainOperationEntr context.Items[typeof(Type)] = domainOperationEntry.AssociatedType; // The principal is retrieved through the DomainServiceContext as a service - IPrincipal? principal = this.ServiceContext != null ? this.ServiceContext.User : null; + IPrincipal? principal = this.ServiceContext.User; // Null principal is denied before going any further -- it is contractually required for the // authorization attributes. From 7f077b413d744d5573b72264bfc13d0a75ab8cff Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Thu, 23 Apr 2026 22:30:44 +0200 Subject: [PATCH 16/19] FIx failing test --- src/OpenRiaServices.Client/Framework/Polyfills.cs | 2 ++ src/OpenRiaServices.Client/Framework/SubmitResult.cs | 2 -- .../OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/OpenRiaServices.Client/Framework/Polyfills.cs b/src/OpenRiaServices.Client/Framework/Polyfills.cs index 5bf000a4d..d66303912 100644 --- a/src/OpenRiaServices.Client/Framework/Polyfills.cs +++ b/src/OpenRiaServices.Client/Framework/Polyfills.cs @@ -14,6 +14,7 @@ internal static class Polyfills { extension(ArgumentNullException) { + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) { if (argument is null) @@ -21,6 +22,7 @@ public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpres } #pragma warning disable CS8777 // Parameter must have a non-null value when exiting. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) { if (string.IsNullOrEmpty(argument)) diff --git a/src/OpenRiaServices.Client/Framework/SubmitResult.cs b/src/OpenRiaServices.Client/Framework/SubmitResult.cs index bdd88c280..65ce2c8d4 100644 --- a/src/OpenRiaServices.Client/Framework/SubmitResult.cs +++ b/src/OpenRiaServices.Client/Framework/SubmitResult.cs @@ -16,8 +16,6 @@ public class SubmitResult /// The changeset which was submitted. public SubmitResult(EntityChangeSet changeSet) { - ArgumentNullException.ThrowIfNull(changeSet); - ChangeSet = changeSet; } diff --git a/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs b/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs index 3efffcb79..4c3491f7f 100644 --- a/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs +++ b/src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs @@ -354,7 +354,8 @@ public async Task ExceptionsFromCallbacks() { try { - var submit = new SubmitOperation(cities.EntityContainer.GetChanges(), soCallback, null, Task.FromResult(new SubmitResult(null)), null); + var changeSet = cities.EntityContainer.GetChanges(); + var submit = new SubmitOperation(changeSet, soCallback, null, Task.FromResult(new SubmitResult(changeSet)), null); } catch (Exception ex) { @@ -378,7 +379,7 @@ public async Task ExceptionsFromCallbacks() var submitTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var submitOperation = new SubmitOperation(cities.EntityContainer.GetChanges(), soCallback, null, submitTaskSource.Task, null); - await AssertExceptionIsCorrectlyRaisedOnSyncContext(submitOperation, submitTaskSource, new SubmitResult(null)); + await AssertExceptionIsCorrectlyRaisedOnSyncContext(submitOperation, submitTaskSource, new SubmitResult(cities.EntityContainer.GetChanges())); // verify cancellation callbacks for all fx operation types var noCompleteLoad = new TaskCompletionSource>(); From 8e510a522d78d167742ac037e7c1786467163208 Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Thu, 23 Apr 2026 22:50:17 +0200 Subject: [PATCH 17/19] add more nullable annotations --- .../ComplexObject.INotifyDataErrorInfo.cs | 8 +- .../Framework/ComplexObject.cs | 65 ++++---- .../Framework/Entity.cs | 148 ++++++++++-------- .../Framework/EntityAction.cs | 32 ++-- .../Framework/EntityCollection.cs | 6 +- .../EntityCollectionChangedEventArgs.cs | 2 + .../Framework/EntityQuery.cs | 1 + .../Framework/EntitySet.cs | 14 +- .../Framework/ILoadResult.cs | 2 + .../Framework/ObjectStateUtility.cs | 56 +++---- .../Framework/ValidationUtilities.cs | 17 +- 11 files changed, 198 insertions(+), 153 deletions(-) diff --git a/src/OpenRiaServices.Client/Framework/ComplexObject.INotifyDataErrorInfo.cs b/src/OpenRiaServices.Client/Framework/ComplexObject.INotifyDataErrorInfo.cs index 3cab1c72c..b88b34d89 100644 --- a/src/OpenRiaServices.Client/Framework/ComplexObject.INotifyDataErrorInfo.cs +++ b/src/OpenRiaServices.Client/Framework/ComplexObject.INotifyDataErrorInfo.cs @@ -8,11 +8,13 @@ using System.Linq; using System.Runtime.Serialization; +#nullable enable + namespace OpenRiaServices.Client { public abstract partial class ComplexObject : INotifyDataErrorInfo { - private EventHandler _errorsChangedHandler; + private EventHandler? _errorsChangedHandler; /// /// Raises the event whenever validation errors have changed for a property. /// @@ -24,7 +26,7 @@ private void RaiseValidationErrorsChanged(string propertyName) /// /// Explicitly implement the event. /// - event EventHandler INotifyDataErrorInfo.ErrorsChanged + event EventHandler? INotifyDataErrorInfo.ErrorsChanged { add { this._errorsChangedHandler += value; } remove { this._errorsChangedHandler -= value; } @@ -42,7 +44,7 @@ event EventHandler INotifyDataErrorInfo.ErrorsChange /// or type-level errors when is /// null or empty. /// - IEnumerable INotifyDataErrorInfo.GetErrors(string propertyName) + IEnumerable INotifyDataErrorInfo.GetErrors(string? propertyName) { IEnumerable results; if (string.IsNullOrEmpty(propertyName)) diff --git a/src/OpenRiaServices.Client/Framework/ComplexObject.cs b/src/OpenRiaServices.Client/Framework/ComplexObject.cs index d1dc0d55c..1118d945f 100644 --- a/src/OpenRiaServices.Client/Framework/ComplexObject.cs +++ b/src/OpenRiaServices.Client/Framework/ComplexObject.cs @@ -9,6 +9,8 @@ using System.Runtime.Serialization; using OpenRiaServices.Client.Internal; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -17,17 +19,17 @@ namespace OpenRiaServices.Client [DataContract] public abstract partial class ComplexObject : INotifyPropertyChanged, IEditableObject { - private PropertyChangedEventHandler _propChangedHandler; - private ComplexObjectValidationResultCollection _validationErrors; - private Dictionary _trackedInstances; - private Action _onDataMemberChanging; - private Action _onDataMemberChanged; - private Action> _onMemberValidationChanged; - private EditSession _editSession; - private object _parent; - private string _parentPropertyName; + private PropertyChangedEventHandler? _propChangedHandler; + private ComplexObjectValidationResultCollection? _validationErrors; + private Dictionary? _trackedInstances; + private Action? _onDataMemberChanging; + private Action? _onDataMemberChanged; + private Action>? _onMemberValidationChanged; + private EditSession? _editSession; + private object? _parent; + private string? _parentPropertyName; private bool _isDeserializing; - private MetaType _metaType; + private MetaType? _metaType; /// /// Gets the map of child ComplexObject instances currently being @@ -49,14 +51,14 @@ private Dictionary TrackedInstances /// Gets the parent entity if this complex type instance is hosted /// by an entity. May return null. /// - private Entity Entity + private Entity? Entity { get { - object currParent = this._parent; + object? currParent = this._parent; while (currParent != null) { - Entity entity = currParent as Entity; + Entity? entity = currParent as Entity; if (entity != null) { return entity; @@ -90,6 +92,11 @@ internal MetaType MetaType /// Gets a value indicating whether this complex type instance is currently /// attached to a parent. /// + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(_parent))] + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(_parentPropertyName))] + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(_onDataMemberChanging))] + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(_onDataMemberChanged))] + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(_onMemberValidationChanged))] internal bool IsAttached { get @@ -206,7 +213,7 @@ internal void Detach() /// /// Event raised whenever a property has changed. /// - event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged + event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged { add { this._propChangedHandler += value; } remove { this._propChangedHandler -= value; } @@ -278,7 +285,7 @@ private void AttachComplexObjectInstance(MetaMember metaMember) // First check if the parent has an existing instance attached for this // property and detach if necessary. string memberName = metaMember.Name; - ComplexObject prevInstance = null; + ComplexObject? prevInstance = null; if (this.TrackedInstances.TryGetValue(memberName, out prevInstance)) { prevInstance.Detach(); @@ -366,7 +373,7 @@ private void OnMemberValidationChanged(string propertyName, IEnumerable validationResults) + private void NotifyParentMemberValidationChanged(string? propertyName, IEnumerable validationResults) { if (this.IsAttached) { @@ -406,7 +413,7 @@ protected void ValidateProperty(string propertyName, object value) // if this instance is currently being deserialized, or if it is hosted by an // entity that is being deserialized or is having state applied to it, skip // validation - Entity rootEntity = this.Entity; + Entity? rootEntity = this.Entity; if (this.IsDeserializing || (rootEntity != null && (rootEntity.IsDeserializing || rootEntity.IsApplyingState))) { return; @@ -440,7 +447,7 @@ protected void ValidateProperty(string propertyName, object value) throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.Property_Is_ReadOnly, propertyName)); } - ComplexObject complexObject = value as ComplexObject; + ComplexObject? complexObject = value as ComplexObject; if (complexObject != null && complexObject.IsAttached) { throw new InvalidOperationException(Resource.ComplexType_InstancesCannotBeShared); @@ -482,7 +489,7 @@ protected virtual void ValidateProperty(ValidationContext validationContext, obj Validator.TryValidateProperty(value, validationContext, validationResults); // Process the validation the errors for this property - this.OnMemberValidationChanged(validationContext.MemberName, validationResults); + this.OnMemberValidationChanged(validationContext.MemberName!, validationResults); } /// @@ -498,8 +505,8 @@ private ValidationContext CreateValidationContext() { // Get the validation context from the entity container if available, // otherwise create a new context. - ValidationContext parentContext = null; - Entity rootEntity = this.Entity; + ValidationContext? parentContext = null; + Entity? rootEntity = this.Entity; if (rootEntity != null && rootEntity.EntitySet != null && rootEntity.EntitySet.EntityContainer != null) { parentContext = rootEntity.EntitySet.EntityContainer.ValidationContext; @@ -540,7 +547,7 @@ private void CheckForCycles(object candidateParent) // Walk up the containment chain searching for this instance. // If found, setting the parent of this instance to the candidate // would result in a cycle. - object currParent = candidateParent; + object? currParent = candidateParent; while (currParent != null) { if (currParent == this) @@ -548,8 +555,7 @@ private void CheckForCycles(object candidateParent) throw new InvalidOperationException(Resource.CyclicReferenceError); } - ComplexObject complexParent = currParent as ComplexObject; - if (complexParent != null) + if (currParent is ComplexObject complexParent) { currParent = complexParent._parent; } @@ -586,6 +592,7 @@ internal void VerifyNotEditing() /// edit session in progress for this instance. This is the case when /// BeginEdit has been called, but EndEdit/CancelEdit have not. /// + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(_editSession))] internal bool IsEditing { get @@ -681,7 +688,7 @@ protected void EndEdit() private class EditSession { private readonly ComplexObject _instance; - private IDictionary _snapshot; + private IDictionary? _snapshot; private readonly ValidationResult[] _validationErrors; private EditSession(ComplexObject instance) @@ -778,18 +785,18 @@ protected override void OnRemove(ValidationResult item) // search (by value) for the item in our parents error collection ICollection resultCollection = GetValidationResults(this._complexObject._parent); ValidationResultEqualityComparer comparer = new ValidationResultEqualityComparer(); - item = resultCollection.FirstOrDefault(p => comparer.Equals(p, item)); + var existing = resultCollection.FirstOrDefault(p => comparer.Equals(p, item)); - if (item != null) + if (existing != null) { - resultCollection.Remove(item); + resultCollection.Remove(existing); } } } private static ICollection GetValidationResults(object parent) { - ComplexObject complexParent = parent as ComplexObject; + ComplexObject? complexParent = parent as ComplexObject; if (complexParent != null) { return complexParent.ValidationErrors; diff --git a/src/OpenRiaServices.Client/Framework/Entity.cs b/src/OpenRiaServices.Client/Framework/Entity.cs index 71309dc3c..544caab6b 100644 --- a/src/OpenRiaServices.Client/Framework/Entity.cs +++ b/src/OpenRiaServices.Client/Framework/Entity.cs @@ -11,6 +11,8 @@ using System.Runtime.Serialization; using OpenRiaServices.Client.Internal; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -19,17 +21,17 @@ namespace OpenRiaServices.Client [DataContract] public abstract partial class Entity : IEditableObject, INotifyPropertyChanged, IRevertibleChangeTracking { - private Action _setChangedCallback; - private EditSession _editSession; - private List _customMethodInvocations; - private EntityConflict _conflict; - private EntitySet _lastSet; - private EntitySet _entitySet; + private Action? _setChangedCallback; + private EditSession? _editSession; + private List? _customMethodInvocations; + private EntityConflict? _conflict; + private EntitySet? _lastSet; + private EntitySet? _entitySet; private EntityState _entityState = EntityState.Detached; - private IDictionary _originalValues; - private Dictionary _entityRefs; - private PropertyChangedEventHandler _propChangedHandler; - private EntityValidationResultCollection _validationErrors; + private IDictionary? _originalValues; + private Dictionary? _entityRefs; + private PropertyChangedEventHandler? _propChangedHandler; + private EntityValidationResultCollection? _validationErrors; private bool _isApplyingState; private bool _isDeserializing; private bool _isInferred; @@ -37,11 +39,11 @@ public abstract partial class Entity : IEditableObject, INotifyPropertyChanged, private bool _isSubmitting; private bool _trackChanges; - private Entity _parent; - private EntityAssociationAttribute _parentAssociation; + private Entity? _parent; + private EntityAssociationAttribute? _parentAssociation; private bool _hasChildChanges; - private Dictionary _trackedInstances; - private MetaType _metaType; + private Dictionary? _trackedInstances; + private MetaType? _metaType; /// /// "EntityState" @@ -75,7 +77,7 @@ private Dictionary TrackedInstances /// Gets the parent of this entity, if this entity is part of /// a composition relationship. /// - internal Entity Parent + internal Entity? Parent { get { @@ -86,7 +88,7 @@ internal Entity Parent /// /// Gets the parent association for this entity. /// - internal EntityAssociationAttribute ParentAssociation + internal EntityAssociationAttribute? ParentAssociation { get { @@ -141,7 +143,7 @@ internal void SetParent(Entity parent, EntityAssociationAttribute association) /// operation. Returns null if there are no conflicts. /// [Display(AutoGenerateField = false)] - public EntityConflict EntityConflict + public EntityConflict? EntityConflict { get { @@ -162,6 +164,7 @@ internal set /// edit session in progress for this entity. This is the case when /// BeginEdit has been called, but EndEdit/CancelEdit have not. /// + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(_editSession))] internal bool IsEditing { get @@ -247,7 +250,7 @@ private set this.RaisePropertyChanged(EntityStatePropertyName); // track or untrack this Entity as required - EntitySet entitySet = this.LastSet; + EntitySet? entitySet = this.LastSet; if (entitySet != null) { bool isInteresting = this._entityState is not EntityState.Unmodified @@ -346,7 +349,7 @@ internal IEnumerable ModifiedProperties /// Gets the this is a member of. The value will be null /// if the entity is Detached or has been removed from the set. /// - protected internal EntitySet EntitySet + protected internal EntitySet? EntitySet { get { @@ -396,7 +399,7 @@ private void RaiseCanInvokeChanged() /// this member, however in some cases (e.g. for deleted entities) /// EntitySet will be null and we need to get back to the set. /// - internal EntitySet LastSet + internal EntitySet? LastSet { get { @@ -408,7 +411,7 @@ internal EntitySet LastSet /// Gets or sets the custom method invocation on this entity (if any) /// while bypassing lots of the validation (this is only used by the old tests) /// - internal EntityAction CustomMethodInvocation + internal EntityAction? CustomMethodInvocation { set { @@ -467,7 +470,7 @@ private void UndoAllEntityActions(bool preventRaiseReadOnly = false, bool throwI /// /// Gets the original values of this Entity's properties before modification /// - internal IDictionary OriginalValues + internal IDictionary? OriginalValues { get { @@ -621,14 +624,14 @@ internal protected virtual void OnLoaded(bool isInitialLoad) /// Gets the original state for this entity. /// /// The entity in its original state if the entity has had property modifications, null otherwise. - public Entity GetOriginal() + public Entity? GetOriginal() { if (this.OriginalValues == null) { return null; } - Entity original = (Entity)Activator.CreateInstance(this.GetType()); + Entity original = (Entity)Activator.CreateInstance(this.GetType())!; original.ApplyState(this.OriginalValues); return original; } @@ -684,7 +687,7 @@ protected void AcceptChanges() return; } - EntitySet entitySet = this.LastSet; + EntitySet? entitySet = this.LastSet; // Accept any child changes. Note, we must accept child changes before setting our own // state to Unmodified. This avoids a situation where we get notifications from child @@ -758,7 +761,7 @@ protected void RejectChanges() return; } - EntitySet entitySet = this.LastSet; + EntitySet? entitySet = this.LastSet; // Reject any child changes. Note, we must reject child changes before setting our own // state to Unmodified. This avoids a situation where we get notifications from child @@ -869,7 +872,7 @@ internal void MarkDetached() this.EntityState = EntityState.Detached; } - internal IDictionary ExtractState() + internal IDictionary ExtractState() { return ObjectStateUtility.ExtractState(this); } @@ -909,10 +912,10 @@ internal void SetEntityRef(string memberName, IEntityRef entityRef) /// store values. /// /// IDictionary with the new original state. - internal void UpdateOriginalValues(IDictionary entityStateToApply) + internal void UpdateOriginalValues(IDictionary entityStateToApply) { Debug.Assert(this._originalValues != null, "Should only call UpdateOriginalValues if the entity has original values."); - this._originalValues = new Dictionary(entityStateToApply); + this._originalValues = new Dictionary(entityStateToApply); } /// @@ -924,9 +927,9 @@ internal void UpdateOriginalValues(IDictionary entityStateToAppl /// yet, null is returned. /// The if the reference has been initialized, /// null otherwise. - internal IEntityRef GetEntityRef(string memberName) + internal IEntityRef? GetEntityRef(string memberName) { - IEntityRef entityRef = null; + IEntityRef? entityRef = null; this.EntityRefs.TryGetValue(memberName, out entityRef); return entityRef; } @@ -936,12 +939,12 @@ internal IEntityRef GetEntityRef(string memberName) /// merge strategy and normal change tracking. /// /// The state to apply - internal void ApplyState(IDictionary entityStateToApply) + internal void ApplyState(IDictionary entityStateToApply) { this.ApplyState(entityStateToApply, LoadBehavior.RefreshCurrent); } - internal void ApplyState(IDictionary entityStateToApply, LoadBehavior loadBehavior) + internal void ApplyState(IDictionary entityStateToApply, LoadBehavior loadBehavior) { if (loadBehavior == LoadBehavior.KeepCurrent) { @@ -997,7 +1000,7 @@ internal void Merge(Entity otherEntity, LoadBehavior loadBehavior) return; } - IDictionary otherState = otherEntity.ExtractState(); + IDictionary otherState = otherEntity.ExtractState(); Merge(otherState, loadBehavior); } @@ -1008,7 +1011,7 @@ internal void Merge(Entity otherEntity, LoadBehavior loadBehavior) /// /// The property values to merge into the current instance /// The load behavior to use - internal void Merge(IDictionary otherState, LoadBehavior loadBehavior) + internal void Merge(IDictionary otherState, LoadBehavior loadBehavior) { if (loadBehavior == LoadBehavior.KeepCurrent) { @@ -1118,15 +1121,13 @@ private void AttachComplexObjectInstance(MetaMember metaMember) // First check if the parent has an existing instance attached for this // property and detach if necessary. string memberName = metaMember.Name; - ComplexObject prevInstance = null; - if (this.TrackedInstances.TryGetValue(memberName, out prevInstance)) + if (this.TrackedInstances.TryGetValue(memberName, out ComplexObject? prevInstance)) { prevInstance.Detach(); this.TrackedInstances.Remove(memberName); } - ComplexObject newInstance = (ComplexObject)metaMember.GetValue(this); - if (newInstance != null) + if (metaMember.GetValue(this) is ComplexObject newInstance) { // Attach to the new instance newInstance.Attach(this, memberName, this.OnDataMemberChanging, this.OnDataMemberChanged, this.OnMemberValidationChanged); @@ -1198,7 +1199,7 @@ protected void RaiseDataMemberChanging(string propertyName) private void OnDataMemberChanging() { - EntitySet set = this.LastSet; + EntitySet? set = this.LastSet; if (set != null) { // During merge operations, we want to suspend change tracking @@ -1235,7 +1236,7 @@ private void OnDataMemberChanging() /// is thrown if is null or empty. /// is thrown if this property is marked with /// configured to prevent editing. - protected void ValidateProperty(string propertyName, object value) + protected void ValidateProperty(string propertyName, object? value) { if (string.IsNullOrEmpty(propertyName)) { @@ -1308,7 +1309,7 @@ protected void ValidateProperty(string propertyName, object value) /// /// The value to test. It may be null if null is valid for the given property. /// is thrown if is null. - protected virtual void ValidateProperty(ValidationContext validationContext, object value) + protected virtual void ValidateProperty(ValidationContext validationContext, object? value) { if (validationContext == null) { @@ -1335,7 +1336,7 @@ private ValidationContext CreateValidationContext() { // Get the validation context from the entity container if available, // otherwise create a new context. - ValidationContext parentContext = null; + ValidationContext? parentContext = null; if (this.EntitySet != null && this.EntitySet.EntityContainer != null) { @@ -1435,7 +1436,7 @@ private void UpdateRelatedAssociations(IEnumerable modifiedProperties) /// /// The name of the action to invoke /// The parameters values to invoke the specified action with - protected internal void InvokeAction(string actionName, params object[] parameters) + protected internal void InvokeAction(string actionName, params object?[] parameters) { if (string.IsNullOrEmpty(actionName)) { @@ -1542,12 +1543,12 @@ private void UndoAction(EntityAction action, bool throwIfSubmitting) if (!removed) throw new ArgumentException(Resource.Entity_UndoInvokeOnlyForInvokedActions); - var customUpdate = MetaType.GetEntityAction(action.Name); - Debug.Assert(customUpdate != null, "EntityAction have valid name since it is part of EntityActions"); + // Will not be null since action was part of _customMethodInvocations, where only valid actions are added + EntityActionAttribute customUpdate = MetaType.GetEntityAction(action.Name); RaisePropertyChanged(customUpdate.CanInvokePropertyName); // If no additional invocations are recorded, then raise an update - if (!_customMethodInvocations.Any(item => item.Name == action.Name)) + if (!_customMethodInvocations!.Any(item => item.Name == action.Name)) RaisePropertyChanged(customUpdate.IsInvokedPropertyName); } @@ -1604,7 +1605,7 @@ public IEnumerable EntityActions /// does not yet have an identity, null will be returned. /// /// The identity for this entity - public virtual object GetIdentity() + public virtual object? GetIdentity() { var keyMembers = this.MetaType.KeyMembers; @@ -1619,7 +1620,7 @@ public virtual object GetIdentity() object[] keyValues = new object[keyMembers.Count]; for (int i = 0; i < keyMembers.Count; i++) { - object keyValue = keyMembers[i].GetValue(this); + object? keyValue = keyMembers[i].GetValue(this); if (keyValue == null) { return null; @@ -1641,8 +1642,8 @@ public override string ToString() if (keyMembers.Count == 1) { - object keyValue = keyMembers[0].GetValue(this); - keyText = keyValue != null ? keyValue.ToString() : "null"; + object? keyValue = keyMembers[0].GetValue(this); + keyText = keyValue?.ToString() ?? "null"; } else { @@ -1653,8 +1654,8 @@ public override string ToString() { sb.Append(','); } - object keyValue = keyMember.GetValue(this); - sb.Append(keyValue != null ? keyValue.ToString() : "null"); + object? keyValue = keyMember.GetValue(this); + sb.Append(keyValue?.ToString() ?? "null"); } keyText = keyMembers.Count > 1 ? "{" + sb.ToString() + "}" : sb.ToString(); } @@ -1676,15 +1677,15 @@ internal void RegisterSetChangedCallback(Action callback) /// /// Event raised whenever an property has changed /// - public event PropertyChangedEventHandler PropertyChanged + public event PropertyChangedEventHandler? PropertyChanged { add { - this._propChangedHandler = (PropertyChangedEventHandler)Delegate.Combine(this._propChangedHandler, value); + this._propChangedHandler = (PropertyChangedEventHandler?)Delegate.Combine(this._propChangedHandler, value); } remove { - this._propChangedHandler = (PropertyChangedEventHandler)Delegate.Remove(this._propChangedHandler, value); + this._propChangedHandler = (PropertyChangedEventHandler?)Delegate.Remove(this._propChangedHandler, value); } } #endregion @@ -1810,8 +1811,8 @@ private class EditSession { private readonly Entity _entity; private readonly EntityState _lastState; - private IDictionary _snapshot; - private readonly EntityAction[] _customMethodInvocations; + private IDictionary? _snapshot; + private readonly EntityAction[]? _customMethodInvocations; private readonly ValidationResult[] _validationErrors; private readonly List _modifiedProperties; @@ -1952,22 +1953,39 @@ protected override void OnPropertyErrorsChanged(string propertyName) namespace OpenRiaServices.Client.EntityExtensions { + /// + /// Allow access to advanced entity state management capabilities such as ExtractState, ApplyState, Merge, and UpdateOriginalValues through extension methods on Entity. + /// public static class EntityExtensions { /// - public static IDictionary ExtractState(this Entity targetEntity) + public static IDictionary ExtractState(this Entity targetEntity) { return targetEntity.ExtractState(); } + /// + [Obsolete("Use ApplyState to instead.")] + public static void ExtractState(this Entity targetEntity, IDictionary entityStateToApply) + { + targetEntity.ApplyState(entityStateToApply); + } + + /// + [Obsolete("Use ApplyState to instead.")] + public static void ExtractState(this Entity targetEntity, IDictionary entityStateToApply, LoadBehavior loadBehavior) + { + targetEntity.ApplyState(entityStateToApply, loadBehavior); + } + /// - public static void ExtractState(this Entity targetEntity, IDictionary entityStateToApply) + public static void ApplyState(this Entity targetEntity, IDictionary entityStateToApply) { targetEntity.ApplyState(entityStateToApply); } /// - public static void ExtractState(this Entity targetEntity, IDictionary entityStateToApply, LoadBehavior loadBehavior) + public static void ApplyState(this Entity targetEntity, IDictionary entityStateToApply, LoadBehavior loadBehavior) { targetEntity.ApplyState(entityStateToApply, loadBehavior); } @@ -1979,7 +1997,7 @@ public static void UpdateOriginalValues(this Entity targetEntity, Entity entity) } /// - public static void UpdateOriginalValues(this Entity targetEntity, IDictionary entityStateToApply) + public static void UpdateOriginalValues(this Entity targetEntity, IDictionary entityStateToApply) { targetEntity.UpdateOriginalValues(entityStateToApply); } @@ -1990,14 +2008,14 @@ public static void Merge(this Entity targetEntity, Entity otherEntity, LoadBehav targetEntity.Merge(otherEntity, loadBehavior); } - /// - public static void Merge(this Entity targetEntity, IDictionary otherState, LoadBehavior loadBehavior) + /// + public static void Merge(this Entity targetEntity, IDictionary otherState, LoadBehavior loadBehavior) { targetEntity.Merge(otherState, loadBehavior); } /// - public static EntitySet GetEntitySet(this TEntity entity) where TEntity : Entity + public static EntitySet? GetEntitySet(this TEntity entity) where TEntity : Entity { return entity.EntitySet as EntitySet; } diff --git a/src/OpenRiaServices.Client/Framework/EntityAction.cs b/src/OpenRiaServices.Client/Framework/EntityAction.cs index 385f11c55..b11a1aca7 100644 --- a/src/OpenRiaServices.Client/Framework/EntityAction.cs +++ b/src/OpenRiaServices.Client/Framework/EntityAction.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; + +#nullable enable namespace OpenRiaServices.Client { @@ -7,22 +10,31 @@ namespace OpenRiaServices.Client /// public class EntityAction { - private readonly List _parameters; + private readonly object?[] _parameters; /// /// Initializes a new instance of the EntityAction class /// /// Name of the entity action /// The parameters to pass to the entity action - public EntityAction(string name, params object[] parameters) + public EntityAction(string name, params object?[] parameters) { this.Name = name; - this._parameters = new List(); - if (parameters != null) - { - this._parameters.AddRange(parameters); - } + this._parameters = (parameters is not null) ? [.. parameters] : []; + } + +#if NET + /// + /// Initializes a new instance of the EntityAction class + /// + /// Name of the entity action + /// The parameters to pass to the entity action + public EntityAction(string name, params ReadOnlySpan parameters) + { + this.Name = name; + this._parameters = parameters.Length > 0 ? [.. parameters] : []; } +#endif /// /// Gets the name of the entity action @@ -36,7 +48,7 @@ public string Name /// /// Gets the parameters to pass to the entity action /// - public IEnumerable Parameters + public IEnumerable Parameters { get { @@ -51,7 +63,7 @@ public bool HasParameters { get { - return (this._parameters.Count > 0); + return (this._parameters.Length > 0); } } } diff --git a/src/OpenRiaServices.Client/Framework/EntityCollection.cs b/src/OpenRiaServices.Client/Framework/EntityCollection.cs index 287870111..4c8d2edde 100644 --- a/src/OpenRiaServices.Client/Framework/EntityCollection.cs +++ b/src/OpenRiaServices.Client/Framework/EntityCollection.cs @@ -194,7 +194,7 @@ private bool IsSourceExternal { get { - return this.SourceSet != null && this.SourceSet.EntityContainer != this._parent.EntitySet.EntityContainer; + return this.SourceSet != null && this.SourceSet.EntityContainer != this._parent.EntitySet!.EntityContainer; } } @@ -278,7 +278,7 @@ public void Add(TEntity entity) if (this.IsComposition) { - entity.Parent.OnChildUpdate(); + entity.Parent!.OnChildUpdate(); } } @@ -334,7 +334,7 @@ public void Remove(TEntity entity) this._sourceSet.Remove(entity); } - entity.Parent.OnChildUpdate(); + entity.Parent!.OnChildUpdate(); } } diff --git a/src/OpenRiaServices.Client/Framework/EntityCollectionChangedEventArgs.cs b/src/OpenRiaServices.Client/Framework/EntityCollectionChangedEventArgs.cs index 086fe178b..b2acd3b03 100644 --- a/src/OpenRiaServices.Client/Framework/EntityCollectionChangedEventArgs.cs +++ b/src/OpenRiaServices.Client/Framework/EntityCollectionChangedEventArgs.cs @@ -1,5 +1,7 @@ using System; +#nullable enable + namespace OpenRiaServices.Client { /// diff --git a/src/OpenRiaServices.Client/Framework/EntityQuery.cs b/src/OpenRiaServices.Client/Framework/EntityQuery.cs index 368985605..10406dde2 100644 --- a/src/OpenRiaServices.Client/Framework/EntityQuery.cs +++ b/src/OpenRiaServices.Client/Framework/EntityQuery.cs @@ -195,6 +195,7 @@ internal EntityQuery(EntityQuery eq, IQueryable query) { } + /// /// internal new IQueryable? Query { diff --git a/src/OpenRiaServices.Client/Framework/EntitySet.cs b/src/OpenRiaServices.Client/Framework/EntitySet.cs index 36d501c1a..6694f0744 100644 --- a/src/OpenRiaServices.Client/Framework/EntitySet.cs +++ b/src/OpenRiaServices.Client/Framework/EntitySet.cs @@ -397,7 +397,7 @@ internal void AddInternal(Entity entity) // - scenarios where an entity is removed and a new entity with the same identity is added // - scenarios where the entity instance itself is the one already cached (for infer attach // state transition scenarios) - object identity = entity.GetIdentity(); + object? identity = entity.GetIdentity(); if (identity != null && this._identityCache.TryGetValue(identity, out Entity? cachedEntity) && cachedEntity.EntityState != EntityState.Deleted @@ -626,7 +626,7 @@ internal void AttachInternal(Entity entity) // Throw if the entity identity is null or we already have // an entity cached with the same identity. - object identity = entity.GetIdentity(); + object? identity = entity.GetIdentity(); if (identity == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.EntityKey_NullIdentity, entity)); @@ -739,7 +739,7 @@ internal Entity LoadEntity(Entity entity, LoadBehavior loadBehavior) throw new InvalidOperationException(Resource.EntitySet_EntityAlreadyAttached); } - object identity = entity.GetIdentity(); + object? identity = entity.GetIdentity(); if (identity == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.EntityKey_NullIdentity, entity)); @@ -812,7 +812,7 @@ internal Entity LoadEntity(Entity entity, LoadBehavior loadBehavior) /// The entity to add internal void AddToCache(Entity entity) { - object identity = entity.GetIdentity(); + object? identity = entity.GetIdentity(); if (identity == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.EntityKey_NullIdentity, entity)); @@ -831,7 +831,7 @@ internal void AddToCache(Entity entity) /// The entity to remove. internal void RemoveFromCache(Entity entity) { - object identity = entity.GetIdentity(); + object? identity = entity.GetIdentity(); Entity? cachedEntity; if (identity == null || !this._identityCache.TryGetValue(identity, out cachedEntity) || cachedEntity != entity) @@ -1200,7 +1200,7 @@ protected override void VisitEntityRef(IEntityRef? entityRef, Entity parent, Met // If the EntityRef hasn't been accesssed before, it might // not be initialized yet. In this case we need to access // the property directly. - child = (Entity)member.GetValue(parent); + child = (Entity?)member.GetValue(parent); } else { @@ -1258,7 +1258,7 @@ protected override void VisitEntityRef(IEntityRef? entityRef, Entity parent, Met // If the EntityRef hasn't been accessed before, it might // not be initialized yet. In this case we need to access // the property directly. - child = (Entity)member.GetValue(parent); + child = (Entity?)member.GetValue(parent); } else { diff --git a/src/OpenRiaServices.Client/Framework/ILoadResult.cs b/src/OpenRiaServices.Client/Framework/ILoadResult.cs index 235a7e1ca..ed8381e58 100644 --- a/src/OpenRiaServices.Client/Framework/ILoadResult.cs +++ b/src/OpenRiaServices.Client/Framework/ILoadResult.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +#nullable enable + namespace OpenRiaServices.Client { /// diff --git a/src/OpenRiaServices.Client/Framework/ObjectStateUtility.cs b/src/OpenRiaServices.Client/Framework/ObjectStateUtility.cs index 11645b2ca..6009b3c97 100644 --- a/src/OpenRiaServices.Client/Framework/ObjectStateUtility.cs +++ b/src/OpenRiaServices.Client/Framework/ObjectStateUtility.cs @@ -5,6 +5,8 @@ using System.Reflection; using OpenRiaServices.Client.Internal; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -18,15 +20,15 @@ internal static class ObjectStateUtility /// /// The object to extract state for. /// The state dictionary. - internal static IDictionary ExtractState(object o) + internal static IDictionary ExtractState(object o) { return ExtractState(o, new HashSet()); } - private static Dictionary ExtractState(object o, HashSet visited) + private static Dictionary ExtractState(object o, HashSet visited) { MetaType metaType = MetaType.GetMetaType(o.GetType()); - Dictionary extractedState = new Dictionary(); + Dictionary extractedState = new Dictionary(); if (!visited.Add(o)) { @@ -35,7 +37,7 @@ private static Dictionary ExtractState(object o, HashSet foreach (MetaMember metaMember in metaType.DataMembers) { - object value = metaMember.GetValue(o); + object? value = metaMember.GetValue(o); if (value != null && metaMember.IsComplex && !metaMember.IsCollection) { @@ -53,7 +55,7 @@ private static Dictionary ExtractState(object o, HashSet /// /// The object to apply state to. /// The state dictionary. - internal static void ApplyState(object o, IDictionary stateToApply) + internal static void ApplyState(object o, IDictionary stateToApply) { ObjectStateUtility.ApplyState(o, stateToApply, null, LoadBehavior.RefreshCurrent); } @@ -65,7 +67,7 @@ internal static void ApplyState(object o, IDictionary stateToApp /// The state dictionary. /// The original state map for the modified object. /// The LoadBehavior to govern property merge behavior. - internal static void ApplyState(object o, IDictionary stateToApply, IDictionary originalState, LoadBehavior loadBehavior) + internal static void ApplyState(object o, IDictionary stateToApply, IDictionary? originalState, LoadBehavior loadBehavior) { if (loadBehavior == LoadBehavior.KeepCurrent) { @@ -74,27 +76,27 @@ internal static void ApplyState(object o, IDictionary stateToApp MetaType metaType = MetaType.GetMetaType(o.GetType()); - bool isMerging = (o as Entity) == null ? (o as ComplexObject) != null && (o as ComplexObject).IsMergingState : (o as Entity).IsMergingState; + bool isMerging = (o is Entity entity) ? entity.IsMergingState : (o is ComplexObject co) && co.IsMergingState; foreach (MetaMember metaMember in metaType.DataMembers) { - object newValue; + object? newValue; if ((isMerging && metaMember.IsMergable || !isMerging) && stateToApply.TryGetValue(metaMember.Name, out newValue)) { if (newValue != null && metaMember.IsComplex && !metaMember.IsCollection) { - object currValue = metaMember.GetValue(o); - IDictionary newValueState = (IDictionary)newValue; + object? currValue = metaMember.GetValue(o); + IDictionary newValueState = (IDictionary)newValue; if (currValue != null) { // if the current and new values are both non-null, we have to do a merge - object complexTypeOriginalValues = null; + object? complexTypeOriginalValues = null; if (originalState != null) { originalState.TryGetValue(metaMember.Name, out complexTypeOriginalValues); } - ApplyState(currValue, newValueState, (IDictionary)complexTypeOriginalValues, loadBehavior); + ApplyState(currValue, newValueState, (IDictionary?)complexTypeOriginalValues, loadBehavior); } else { @@ -103,7 +105,7 @@ internal static void ApplyState(object o, IDictionary stateToApp delegate { // Rehydrate an instance from the state dictionary. - object newInstance = Activator.CreateInstance(metaMember.PropertyType); + object newInstance = Activator.CreateInstance(metaMember.PropertyType)!; ApplyState(newInstance, newValueState); return newInstance; }); @@ -126,14 +128,14 @@ internal static void ApplyState(object o, IDictionary stateToApp /// The property to apply the value to /// The original state map for the object /// The LoadBehavior to govern property merge behavior. - private static void ApplyValue(object o, object value, MetaMember member, IDictionary originalState, LoadBehavior loadBehavior) + private static void ApplyValue(object o, object? value, MetaMember member, IDictionary? originalState, LoadBehavior loadBehavior) { if (loadBehavior == LoadBehavior.KeepCurrent) { return; } - Lazy lazyValue = value as Lazy; + Lazy? lazyValue = value as Lazy; if (loadBehavior == LoadBehavior.RefreshCurrent) { // overwrite value with the new value @@ -164,15 +166,15 @@ private static void ApplyValue(object o, object value, MetaMember member, IDicti /// The original values for the modified parent instance. /// The property to check. /// True if the property has changed, false otherwise. - internal static bool PropertyHasChanged(object o, IDictionary originalValues, MetaMember member) + internal static bool PropertyHasChanged(object o, IDictionary? originalValues, MetaMember member) { if (originalValues == null) { return false; } - object currentValue = member.GetValue(o); - object originalValue; + object? currentValue = member.GetValue(o); + object? originalValue; if (!originalValues.TryGetValue(member.Name, out originalValue)) { @@ -195,10 +197,10 @@ internal static bool PropertyHasChanged(object o, IDictionary or /// The Type the state map is for. /// The original state map. /// The state map containing only values that should be rountripped. - internal static IDictionary ExtractRoundtripState(Type type, IDictionary state) + internal static IDictionary ExtractRoundtripState(Type type, IDictionary state) { MetaType metaType = MetaType.GetMetaType(type); - Dictionary resultRoundtripState = new Dictionary(); + Dictionary resultRoundtripState = new Dictionary(); foreach (MetaMember metaMember in metaType.DataMembers) { @@ -218,30 +220,30 @@ internal static IDictionary ExtractRoundtripState(Type type, IDi // if the member is complex we need to preprocess and apply values recursively if (!metaMember.IsCollection) { - IDictionary originalState = (IDictionary)state[metaMember.Name]; + IDictionary? originalState = (IDictionary?)state[metaMember.Name]; if (originalState != null) { - IDictionary roundtripState = ExtractRoundtripState(metaMember.PropertyType, originalState); + IDictionary roundtripState = ExtractRoundtripState(metaMember.PropertyType, originalState); resultRoundtripState.Add(metaMember.Name, roundtripState); } } else { - IEnumerable originalCollection = (IEnumerable)state[metaMember.Name]; + IEnumerable? originalCollection = (IEnumerable?)state[metaMember.Name]; if (originalCollection != null) { Type elementType = TypeUtility.GetElementType(metaMember.PropertyType); - IList newCollection = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)); + IList newCollection = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType))!; // Create a copy collection and copy elements recursively. Since Entity Extract/Apply state isn't // deep through complex type collections, we have to recursively do the RTO filtering here. foreach (object element in originalCollection) { - IDictionary originalState = ObjectStateUtility.ExtractState(element); + IDictionary originalState = ObjectStateUtility.ExtractState(element); if (originalState != null) { - IDictionary roundtripState = ExtractRoundtripState(elementType, originalState); - object newInstance = Activator.CreateInstance(elementType); + IDictionary roundtripState = ExtractRoundtripState(elementType, originalState); + object newInstance = Activator.CreateInstance(elementType)!; ObjectStateUtility.ApplyState(newInstance, roundtripState); newCollection.Add(newInstance); } diff --git a/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs b/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs index a3ba15e25..dca8fc179 100644 --- a/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs +++ b/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs @@ -40,7 +40,6 @@ internal static ValidationContext CreateValidationContext(object instance, Valid ValidationContext context = new ValidationContext(instance, parentContext, parentContext != null ? parentContext.Items : null); return context; } -#nullable restore /// /// Internal helper method for getting a method from an object instance that matches @@ -51,7 +50,7 @@ internal static ValidationContext CreateValidationContext(object instance, Valid /// The parameter values to be passed to the method /// A from an object instance that matches /// the specified parameters. - internal static MethodInfo GetMethod(object instance, string methodName, object[] parameters) + internal static MethodInfo GetMethod(object instance, string methodName, object?[]? parameters) { Type instanceType = instance.GetType(); MethodInfo[] candidates = instanceType.GetMethods() @@ -68,7 +67,7 @@ internal static MethodInfo GetMethod(object instance, string methodName, object[ else { // convert parameter types into a string of this format e.g. ('string', null, 'int') - string[] parameterTypes = parameters.Select(p => ((p == null) ? "null" : string.Format(CultureInfo.InvariantCulture, "'{0}'", p.GetType().ToString()))).ToArray(); + string[] parameterTypes = parameters!.Select(p => ((p == null) ? "null" : string.Format(CultureInfo.InvariantCulture, "'{0}'", p.GetType().ToString()))).ToArray(); throw new MissingMethodException(string.Format(CultureInfo.CurrentCulture, DataResource.ValidationUtilities_MethodNotFound, instanceType, methodName, parameterLength, string.Join(", ", parameterTypes))); } } @@ -80,7 +79,7 @@ internal static MethodInfo GetMethod(object instance, string methodName, object[ return candidates[0]; } - internal static bool IsBindable(Type[] parameterTypes, object[] parameters) + internal static bool IsBindable(Type[] parameterTypes, object?[]? parameters) { int parameterLength = (parameters == null) ? 0 : parameters.Length; if (parameterTypes.Length != parameterLength) @@ -90,7 +89,7 @@ internal static bool IsBindable(Type[] parameterTypes, object[] parameters) for (int i = 0; i < parameterLength; i++) { - if (parameters[i] == null) + if (parameters![i] == null) { if (!TypeUtility.IsNullableType(parameterTypes[i]) && parameterTypes[i].IsValueType) @@ -113,7 +112,7 @@ internal static bool IsBindable(Type[] parameterTypes, object[] parameters) /// The method to validate the set of parameters against. /// The set of parameters to check. /// true if the set of parameters can be passed to the specified method. - internal static bool IsBindable(MethodInfo method, object[] parameters) + internal static bool IsBindable(MethodInfo method, object?[]? parameters) { return IsBindable(method.GetParameters().Select(p => p.ParameterType).ToArray(), parameters); } @@ -124,7 +123,7 @@ internal static bool IsBindable(MethodInfo method, object[] parameters) /// The validation results /// The member path to append /// The updated validation results - internal static IEnumerable ApplyMemberPath(IEnumerable validationResults, string memberPath) + internal static IEnumerable ApplyMemberPath(IEnumerable validationResults, string? memberPath) { if (string.IsNullOrEmpty(memberPath)) { @@ -141,7 +140,7 @@ internal static IEnumerable ApplyMemberPath(IEnumerableThe validation result /// The member path to append /// The updated validation result - internal static ValidationResult ApplyMemberPath(ValidationResult validationResult, string memberPath) + internal static ValidationResult ApplyMemberPath(ValidationResult validationResult, string? memberPath) { if (string.IsNullOrEmpty(memberPath)) { @@ -168,7 +167,7 @@ internal static ValidationResult ApplyMemberPath(ValidationResult validationResu return new ValidationResult(validationResult.ErrorMessage, memberNames); } - +#nullable restore /// /// Validate the specified object an any complex members or collections recursively. /// From d777b0283a940a79ea8eb6db0a84e0b6c9799ac4 Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Fri, 24 Apr 2026 19:38:40 +0200 Subject: [PATCH 18/19] more nullability annotations --- .../ComplexObject.INotifyDataErrorInfo.cs | 2 +- .../Framework/ComplexObject.cs | 5 +- .../Framework/DomainContext.cs | 98 +++++++++---------- .../Framework/DomainIdentifierAttribute.cs | 4 +- .../Framework/Entity.INotifyDataErrorInfo.cs | 2 +- .../Framework/Entity.cs | 4 +- .../Framework/EntityChangeSet.cs | 2 +- .../Framework/ValidationResultCollection.cs | 65 ++++++------ .../Framework/ValidationUtilities.cs | 8 +- 9 files changed, 93 insertions(+), 97 deletions(-) diff --git a/src/OpenRiaServices.Client/Framework/ComplexObject.INotifyDataErrorInfo.cs b/src/OpenRiaServices.Client/Framework/ComplexObject.INotifyDataErrorInfo.cs index b88b34d89..06d05ff81 100644 --- a/src/OpenRiaServices.Client/Framework/ComplexObject.INotifyDataErrorInfo.cs +++ b/src/OpenRiaServices.Client/Framework/ComplexObject.INotifyDataErrorInfo.cs @@ -19,7 +19,7 @@ public abstract partial class ComplexObject : INotifyDataErrorInfo /// Raises the event whenever validation errors have changed for a property. /// /// The property whose errors have changed. - private void RaiseValidationErrorsChanged(string propertyName) + private void RaiseValidationErrorsChanged(string? propertyName) { this._errorsChangedHandler?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); } diff --git a/src/OpenRiaServices.Client/Framework/ComplexObject.cs b/src/OpenRiaServices.Client/Framework/ComplexObject.cs index 1118d945f..b812758a0 100644 --- a/src/OpenRiaServices.Client/Framework/ComplexObject.cs +++ b/src/OpenRiaServices.Client/Framework/ComplexObject.cs @@ -601,6 +601,7 @@ internal bool IsEditing } } + /// protected internal bool IsMergingState { get; set; } /// @@ -784,7 +785,7 @@ protected override void OnRemove(ValidationResult item) // search (by value) for the item in our parents error collection ICollection resultCollection = GetValidationResults(this._complexObject._parent); - ValidationResultEqualityComparer comparer = new ValidationResultEqualityComparer(); + ValidationResultEqualityComparer comparer = ValidationResultEqualityComparer.Instance; var existing = resultCollection.FirstOrDefault(p => comparer.Equals(p, item)); if (existing != null) @@ -827,7 +828,7 @@ protected override void OnHasErrorsChanged() { this._complexObject.RaisePropertyChanged(nameof(HasValidationErrors)); } - protected override void OnPropertyErrorsChanged(string propertyName) + protected override void OnPropertyErrorsChanged(string? propertyName) { this._complexObject.RaiseValidationErrorsChanged(propertyName); } diff --git a/src/OpenRiaServices.Client/Framework/DomainContext.cs b/src/OpenRiaServices.Client/Framework/DomainContext.cs index 9642339b0..1fced39f9 100644 --- a/src/OpenRiaServices.Client/Framework/DomainContext.cs +++ b/src/OpenRiaServices.Client/Framework/DomainContext.cs @@ -12,6 +12,8 @@ using System.Threading; using System.Threading.Tasks; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -29,16 +31,16 @@ public abstract class DomainContext : INotifyPropertyChanged typeof(bool), typeof(Type), typeof(CancellationToken) - }); + })!; private int _activeLoadCount; private readonly DomainClient _domainClient; - private EntityContainer _entityContainer; - private ValidationContext _validationContext; + private EntityContainer? _entityContainer; + private ValidationContext? _validationContext; private bool _isSubmitting; private readonly Dictionary _requiresValidationMap = new Dictionary(); private readonly object _syncRoot = new object(); - private static IDomainClientFactory s_domainClientFactory; + private static IDomainClientFactory? s_domainClientFactory; private static TaskScheduler CurrrentSynchronizationContextTaskScheduler => SynchronizationContext.Current != null ? TaskScheduler.FromCurrentSynchronizationContext() : TaskScheduler.Default; @@ -98,7 +100,7 @@ public static IDomainClientFactory DomainClientFactory /// /// Event raised whenever a property changes /// - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; /// /// Gets the for this context @@ -187,7 +189,7 @@ public bool HasChanges /// making these services and items available to each /// involved in validation. /// - public ValidationContext ValidationContext + public ValidationContext? ValidationContext { get { @@ -258,10 +260,10 @@ public SubmitOperation SubmitChanges() /// Optional user state to associate with the operation. /// /// The . - public virtual SubmitOperation SubmitChanges(Action callback, object userState) + public virtual SubmitOperation SubmitChanges(Action? callback, object? userState) { EntityChangeSet changeSet = this.EntityContainer.GetChanges(); - CancellationTokenSource cts = DomainClient.SupportsCancellation ? new CancellationTokenSource() : null; + CancellationTokenSource? cts = DomainClient.SupportsCancellation ? new CancellationTokenSource() : null; var submitTask = SubmitChangesAsync(cts?.Token ?? CancellationToken.None); return new SubmitOperation(changeSet, callback, userState, submitTask, cts); @@ -398,7 +400,7 @@ async Task SubmitChangesAsyncImplementation(TaskTrue if the query supports composition, false otherwise. /// The query. [EditorBrowsable(EditorBrowsableState.Never)] - protected EntityQuery CreateQuery(string queryName, IDictionary parameters, bool hasSideEffects, bool isComposable) where TEntity : Entity + protected EntityQuery CreateQuery(string queryName, IDictionary? parameters, bool hasSideEffects, bool isComposable) where TEntity : Entity { return new EntityQuery(this.DomainClient, queryName, parameters, hasSideEffects, isComposable); } @@ -443,7 +445,7 @@ public LoadOperation Load(EntityQuery query, bool thr /// The load operation. public LoadOperation Load(EntityQuery query, LoadBehavior loadBehavior, bool throwOnError) where TEntity : Entity { - Action> callback = null; + Action>? callback = null; if (!throwOnError) { callback = (op) => @@ -466,7 +468,7 @@ public LoadOperation Load(EntityQuery query, LoadBeha /// Optional callback to be called when the load operation completes. /// Optional user state. /// The load operation. - public LoadOperation Load(EntityQuery query, Action> callback, object userState) where TEntity : Entity + public LoadOperation Load(EntityQuery query, Action>? callback, object? userState) where TEntity : Entity { return this.Load(query, LoadBehavior.KeepCurrent, callback, userState); } @@ -480,9 +482,9 @@ public LoadOperation Load(EntityQuery query, ActionOptional callback to be called when the load operation completes. /// Optional user state. /// The load operation. - public virtual LoadOperation Load(EntityQuery query, LoadBehavior loadBehavior, Action> callback, object userState) where TEntity : Entity + public virtual LoadOperation Load(EntityQuery query, LoadBehavior loadBehavior, Action>? callback, object? userState) where TEntity : Entity { - CancellationTokenSource cts = DomainClient.SupportsCancellation ? new CancellationTokenSource() : null; + CancellationTokenSource? cts = DomainClient.SupportsCancellation ? new CancellationTokenSource() : null; var loadResult = LoadAsync(query, loadBehavior, cts?.Token ?? CancellationToken.None); return new LoadOperation(query, loadBehavior, callback, userState, loadResult, cts); @@ -497,17 +499,17 @@ public virtual LoadOperation Load(EntityQuery query, /// Optional user state. /// The load operation. [EditorBrowsable(EditorBrowsableState.Never)] - public LoadOperation Load(EntityQuery query, LoadBehavior loadBehavior, Action callback, object userState) + public LoadOperation Load(EntityQuery query, LoadBehavior loadBehavior, Action? callback, object? userState) { // Get MethodInfo for Load(EntityQuery, LoadBehavior, Action>, object, LoadOperation) - var method = new Func, LoadBehavior, Action>, object, LoadOperation>(this.Load); + var method = new Func, LoadBehavior, Action>?, object?, LoadOperation>(this.Load); var loadMethod = method.Method.GetGenericMethodDefinition(); try { return (LoadOperation)loadMethod .MakeGenericMethod(query.EntityType) - .Invoke(this, new object[] { query, loadBehavior, callback, userState }); + .Invoke(this, [query, loadBehavior, callback, userState])!; } catch (TargetInvocationException tie) { @@ -586,12 +588,12 @@ public virtual Task> LoadAsync(EntityQuery async Task> LoadAsyncImplementation(Task queryCompletedResult) { - IReadOnlyCollection loadedEntities = null; - IReadOnlyCollection loadedIncludedEntities = null; - List allLoadedEntities = null; + IReadOnlyCollection loadedEntities; + IReadOnlyCollection loadedIncludedEntities; + Entity[] allLoadedEntities; int totalCount; - QueryCompletedResult results = null; + QueryCompletedResult? results = null; try { // The task is known to be completed so this will never block @@ -603,14 +605,12 @@ async Task> LoadAsyncImplementation(Task(loadedEntities.Count + loadedIncludedEntities.Count); - allLoadedEntities.AddRange(loadedEntities); - allLoadedEntities.AddRange(loadedIncludedEntities); + allLoadedEntities = [.. loadedEntities, .. loadedIncludedEntities]; totalCount = results.TotalCount; } catch (Exception ex) when (!(ex is DomainException || ex is OperationCanceledException || ex.IsFatal())) { - string message = string.Format(Resource.DomainContext_LoadOperationFailed, query.QueryName, ex.Message); + string message = string.Format(CultureInfo.InvariantCulture, Resource.DomainContext_LoadOperationFailed, query.QueryName, ex.Message); throw ex is DomainOperationException domainOperationException ? new DomainOperationException(message, domainOperationException) @@ -623,7 +623,7 @@ async Task> LoadAsyncImplementation(Task errors, Dictionary> entityErrorMap) { // We need to accumulate all the errors on an entity in the entityErrorMap Entity.ValidationErrors are IEnumerable. - Debug.Assert(failedEntity != null, "failedEntity should not be null"); - List entityErrors; - if (!entityErrorMap.TryGetValue(failedEntity, out entityErrors)) + if (!entityErrorMap.TryGetValue(failedEntity, out List? entityErrors)) { entityErrors = errors.Select(e => new ValidationResult(e.Message, e.SourceMemberNames)).ToList(); entityErrorMap[failedEntity] = entityErrors; @@ -713,7 +711,7 @@ private static void AddEntityErrors(Entity failedEntity, IEnumerable(validationResult, new ValidationResultEqualityComparer())) + if (!entityErrors.Contains(validationResult, ValidationResultEqualityComparer.Instance)) { entityErrors.Add(validationResult); } @@ -743,7 +741,7 @@ private static void ApplyMemberSynchronizations(IEnumerable chan /// /// The EntityContainer /// The event args - private void EntityContainerPropertyChanged(object sender, PropertyChangedEventArgs e) + private void EntityContainerPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(HasChanges)) { @@ -765,9 +763,9 @@ private void EntityContainerPropertyChanged(object sender, PropertyChangedEventA /// Optional user state for the operation. /// The invoke operation. [EditorBrowsable(EditorBrowsableState.Never)] - public virtual InvokeOperation InvokeOperation(string operationName, Type returnType, IDictionary parameters, bool hasSideEffects, Action> callback, object userState) + public virtual InvokeOperation InvokeOperation(string operationName, Type returnType, IDictionary? parameters, bool hasSideEffects, Action>? callback, object? userState) { - CancellationTokenSource cts = DomainClient.SupportsCancellation ? new CancellationTokenSource() : null; + CancellationTokenSource? cts = DomainClient.SupportsCancellation ? new CancellationTokenSource() : null; var invokeResult = InvokeOperationAsync(operationName, parameters, hasSideEffects, returnType, cts?.Token ?? CancellationToken.None); @@ -786,7 +784,7 @@ public virtual InvokeOperation InvokeOperation(string operationN /// Optional user state for the operation. /// The invoke operation. [EditorBrowsable(EditorBrowsableState.Never)] - public InvokeOperation InvokeOperation(string operationName, Type returnType, IDictionary parameters, bool hasSideEffects, Action callback, object userState) + public InvokeOperation InvokeOperation(string operationName, Type returnType, IDictionary? parameters, bool hasSideEffects, Action? callback, object? userState) { // We only expect void types for generated code // Use InvokeOperation return type for these @@ -800,7 +798,7 @@ public InvokeOperation InvokeOperation(string operationName, Type returnType, ID { return (InvokeOperation)s_invokeOperationAsync .MakeGenericMethod(returnType) - .Invoke(this, new object[] { operationName, returnType, parameters, hasSideEffects, callback, userState }); + .Invoke(this, [operationName, returnType, parameters, hasSideEffects, callback, userState])!; } catch (TargetInvocationException tie) { @@ -824,7 +822,7 @@ public InvokeOperation InvokeOperation(string operationName, Type returnType, ID /// The invoke operation. [EditorBrowsable(EditorBrowsableState.Never)] public Task InvokeOperationAsync(string operationName, - IDictionary parameters, bool hasSideEffects, + IDictionary? parameters, bool hasSideEffects, CancellationToken cancellationToken) { // Do not do use await since parameter validation are not thrown instantly @@ -849,7 +847,7 @@ public Task InvokeOperationAsync(string operationName, /// cancellation token /// The invoke operation. public Task> InvokeOperationAsync(string operationName, - IDictionary parameters, bool hasSideEffects, + IDictionary? parameters, bool hasSideEffects, CancellationToken cancellationToken) { return InvokeOperationAsync(operationName, parameters, hasSideEffects, typeof(TValue), cancellationToken); @@ -867,7 +865,7 @@ public Task> InvokeOperationAsync(string operationN /// cancellation token /// The invoke operation. protected virtual Task> InvokeOperationAsync(string operationName, - IDictionary parameters, bool hasSideEffects, + IDictionary? parameters, bool hasSideEffects, Type returnType, CancellationToken cancellationToken) { @@ -883,17 +881,17 @@ protected virtual Task> InvokeOperationAsync(string InvokeArgs invokeArgs = new InvokeArgs(operationName, returnType, parameters, hasSideEffects); return this.DomainClient.InvokeAsync(invokeArgs, cancellationToken) - .ContinueWith((Task task, object state) => + .ContinueWith((Task task, object? state) => { InvokeCompletedResult results; - string operation = (string)state; + string operation = (string)state!; try { results = task.GetAwaiter().GetResult(); } catch (Exception ex) when (!(ex is DomainException || ex.IsFatal())) { - string message = string.Format(Resource.DomainContext_InvokeOperationFailed, operation, ex.Message); + string message = string.Format(CultureInfo.InvariantCulture, Resource.DomainContext_InvokeOperationFailed, operation, ex.Message); throw ex is DomainOperationException domainOperationException ? new DomainOperationException(message, domainOperationException) @@ -902,11 +900,11 @@ protected virtual Task> InvokeOperationAsync(string if (results.ValidationErrors.Count == 0) { - return new InvokeResult((TValue)results.ReturnValue); + return new InvokeResult((TValue?)results.ReturnValue!); } else { - string message = string.Format(Resource.DomainContext_InvokeOperationFailed_Validation, operation); + string message = string.Format(CultureInfo.InvariantCulture, Resource.DomainContext_InvokeOperationFailed_Validation, operation); throw new DomainOperationException(message, results.ValidationErrors); } } @@ -954,14 +952,14 @@ private void DecrementLoadCount() /// /// The method to validate. /// The parameters to the method. - protected void ValidateMethod(string methodName, IDictionary parameters) + protected void ValidateMethod(string methodName, IDictionary? parameters) { if (string.IsNullOrEmpty(methodName)) { throw new ArgumentNullException(nameof(methodName)); } - object[] paramValues = parameters != null ? parameters.Values.ToArray() : Array.Empty(); + object?[] paramValues = parameters != null ? parameters.Values.ToArray() : Array.Empty(); if (!this.MethodRequiresValidation(methodName, paramValues)) { // method validation is expensive, so skip it if we can @@ -979,7 +977,7 @@ protected void ValidateMethod(string methodName, IDictionary par /// The change-set to validate. /// The ValidationContext to use. /// True if the change-set is valid, false otherwise. - private bool ValidateChangeSet(EntityChangeSet changeSet, ValidationContext validationContext) + private bool ValidateChangeSet(EntityChangeSet changeSet, ValidationContext? validationContext) { if (!changeSet.Validate(validationContext)) { @@ -999,13 +997,7 @@ private bool ValidateChangeSet(EntityChangeSet changeSet, ValidationContext vali // DomainContext custom methods always differ from the entity version because // the first param is the entity. Ensure the entity is the first param in the list. - object[] parameters = new object[customMethod.Parameters.Count() + 1]; - parameters[0] = entity; - int i = 1; - foreach (var parameter in customMethod.Parameters) - { - parameters[i++] = parameter; - } + object?[] parameters = [entity, ..customMethod.Parameters]; // Validate the method exists. ValidationUtilities.GetMethod(this, customMethod.Name, parameters); @@ -1028,7 +1020,7 @@ private bool ValidateChangeSet(EntityChangeSet changeSet, ValidationContext vali /// The method to check /// The parameter values /// True if the method requires validation, false otherwise. - private bool MethodRequiresValidation(string methodName, object[] paramValues) + private bool MethodRequiresValidation(string methodName, object?[] paramValues) { lock(this._syncRoot) { diff --git a/src/OpenRiaServices.Client/Framework/DomainIdentifierAttribute.cs b/src/OpenRiaServices.Client/Framework/DomainIdentifierAttribute.cs index c65c9c500..fe973e218 100644 --- a/src/OpenRiaServices.Client/Framework/DomainIdentifierAttribute.cs +++ b/src/OpenRiaServices.Client/Framework/DomainIdentifierAttribute.cs @@ -1,5 +1,7 @@ using System; +#nullable enable + namespace OpenRiaServices { #if !SERVERFX @@ -63,7 +65,7 @@ internal set /// /// Gets or sets the type /// - public Type CodeProcessor + public Type? CodeProcessor { get; set; diff --git a/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs b/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs index d029279ac..5fc1a3ac0 100644 --- a/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs +++ b/src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs @@ -17,7 +17,7 @@ public abstract partial class Entity : INotifyDataErrorInfo /// Raises the event whenever validation errors have changed for a property. /// /// The property whose errors have changed. - private void RaiseValidationErrorsChanged(string propertyName) + private void RaiseValidationErrorsChanged(string? propertyName) { this._validationErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); } diff --git a/src/OpenRiaServices.Client/Framework/Entity.cs b/src/OpenRiaServices.Client/Framework/Entity.cs index 544caab6b..175060482 100644 --- a/src/OpenRiaServices.Client/Framework/Entity.cs +++ b/src/OpenRiaServices.Client/Framework/Entity.cs @@ -1470,7 +1470,7 @@ protected internal void InvokeAction(string actionName, params object?[] paramet var customMethodInfo = MetaType.GetEntityAction(actionName); if (customMethodInfo == null) - throw new InvalidOperationException(string.Format(Resource.Entity_NoEntityActionWithName, actionName)); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resource.Entity_NoEntityActionWithName, actionName)); // record invocation on the entity, which does proper state transition and raising property changed events InvokeActionCore(new EntityAction(actionName, parameters), customMethodInfo); @@ -1942,7 +1942,7 @@ protected override void OnHasErrorsChanged() this._entity.RaisePropertyChanged(nameof(HasValidationErrors)); } - protected override void OnPropertyErrorsChanged(string propertyName) + protected override void OnPropertyErrorsChanged(string? propertyName) { this._entity.RaiseValidationErrorsChanged(propertyName); } diff --git a/src/OpenRiaServices.Client/Framework/EntityChangeSet.cs b/src/OpenRiaServices.Client/Framework/EntityChangeSet.cs index 0b1aef4fc..316f9641e 100644 --- a/src/OpenRiaServices.Client/Framework/EntityChangeSet.cs +++ b/src/OpenRiaServices.Client/Framework/EntityChangeSet.cs @@ -169,7 +169,7 @@ internal bool Validate(ValidationContext validationContext) if (validationResults.Count > 0) { // replace the validation errors for the entity - IEnumerable entityErrors = validationResults.Select(err => new ValidationResult(err.ErrorMessage, err.MemberNames)).Distinct(new ValidationResultEqualityComparer()).ToList().AsReadOnly(); + IEnumerable entityErrors = validationResults.Select(err => new ValidationResult(err.ErrorMessage, err.MemberNames)).Distinct(ValidationResultEqualityComparer.Instance).ToList().AsReadOnly(); ValidationUtilities.ApplyValidationErrors(entity, entityErrors); success = false; } diff --git a/src/OpenRiaServices.Client/Framework/ValidationResultCollection.cs b/src/OpenRiaServices.Client/Framework/ValidationResultCollection.cs index 8394aac1d..0fef1fdbb 100644 --- a/src/OpenRiaServices.Client/Framework/ValidationResultCollection.cs +++ b/src/OpenRiaServices.Client/Framework/ValidationResultCollection.cs @@ -5,6 +5,8 @@ using System.Linq; using OpenRiaServices.Client.Internal; +#nullable enable + namespace OpenRiaServices.Client { /// @@ -18,9 +20,9 @@ internal abstract class ValidationResultCollection : ICollection _results; private bool _hasErrors; - private IEnumerable _propertiesInError; - private readonly object _parent; - private readonly MetaType _parentMetaType; + private IEnumerable _propertiesInError; + private readonly object? _parent; + private readonly MetaType? _parentMetaType; #endregion Member Fields @@ -30,11 +32,11 @@ internal abstract class ValidationResultCollection : ICollection class. /// /// The parent instance hosting this collection. - internal ValidationResultCollection(object parent) + internal ValidationResultCollection(object? parent) { this._results = new List(); this._hasErrors = false; - this._propertiesInError = Enumerable.Empty(); + this._propertiesInError = []; this._parent = parent; if (this._parent != null) @@ -78,12 +80,12 @@ internal void ReplaceErrors(string propertyName, IEnumerable n if (this.Count > 0) { - // First determine the set of affected member names. We have to take nested member paths - // into account. + // First determine the set of affected member names. We have to take nested member paths + // into account. affectedMembers.AddRange(this.SelectMany(p => p.MemberNames) - .Where(p => (p != null) + .Where(p => (p != null) && p.StartsWith(propertyName, StringComparison.Ordinal) - // name is exact name propertyName , or contains '.' after property name + // name is exact name propertyName , or contains '.' after property name && (p.Length > propertyName.Length && p[propertyName.Length] == '.'))); removedErrors = _results.RemoveAll(r => r.MemberNames.Any(member => affectedMembers.Contains(member))); @@ -97,7 +99,7 @@ internal void ReplaceErrors(string propertyName, IEnumerable n // Force the properties of the new results to receive notifications, ensuring that the // affected members are included in that list - this.OnCollectionChanged(GetPropertiesInError(newResults).Concat(affectedMembers)); + this.OnCollectionChanged([..GetPropertiesInError(newResults), ..affectedMembers]); } } @@ -108,14 +110,14 @@ internal void ReplaceErrors(string propertyName, IEnumerable n /// The properties directly affected by this change. Error change notifications will always /// be raised for these properties. /// - private void OnCollectionChanged(IEnumerable propertiesAffected) + private void OnCollectionChanged(IEnumerable propertiesAffected) { bool origHasErrors = this._hasErrors; - IEnumerable origPropertiesInError = this._propertiesInError; + IEnumerable origPropertiesInError = this._propertiesInError; // Determine our new state this._hasErrors = (this.Count > 0); - this._propertiesInError = new HashSet(GetPropertiesInError(this)); // HashSet is used to make properties distinct + this._propertiesInError = new HashSet(GetPropertiesInError(this)); // HashSet is used to make properties distinct // Call the notification method if the 'HasErrors' bit has changed if (this._hasErrors != origHasErrors) @@ -126,12 +128,12 @@ private void OnCollectionChanged(IEnumerable propertiesAffected) // Get the combined list of properties affected. // SymmetricExceptWith - to get properties in error but aren't any longer or those newly in error // add all affected by the change - HashSet allPropertiesAffected = new HashSet(origPropertiesInError); + HashSet allPropertiesAffected = new(origPropertiesInError); allPropertiesAffected.SymmetricExceptWith(_propertiesInError); allPropertiesAffected.UnionWith(propertiesAffected); // For each property affected, call the errors changed method - foreach (string propertyName in allPropertiesAffected) + foreach (string? propertyName in allPropertiesAffected) { this.OnPropertyErrorsChanged(propertyName); } @@ -145,22 +147,19 @@ private void OnCollectionChanged(IEnumerable propertiesAffected) /// /// A list of properties in error. This list is not guaranteed to be distinct. /// The errors to scan for the list of properties. - private static IEnumerable GetPropertiesInError(IEnumerable errors) + private static List GetPropertiesInError(IEnumerable? errors) { - IEnumerable propertiesInError = Enumerable.Empty(); + if (errors is null || !errors.Any()) + return []; - if (errors != null) + var propertiesInError = new List(errors.SelectMany(e => e.MemberNames)); + if (errors.Any(e => !e.MemberNames.Any())) { - propertiesInError = errors.SelectMany(e => e.MemberNames); - - if (errors.Any(e => !e.MemberNames.Any())) - { - propertiesInError = propertiesInError.Concat(new string[] { null }); - } + propertiesInError.Add(null); } // Be sure to enumerate the results to prevent delayed execution! - return propertiesInError.ToArray(); + return propertiesInError; } /// @@ -184,7 +183,7 @@ protected virtual void OnHasErrorsChanged() /// property has changed. Overrides do not need to call base. /// /// The name of the property whose validation results have changed - protected virtual void OnPropertyErrorsChanged(string propertyName) + protected virtual void OnPropertyErrorsChanged(string? propertyName) { } @@ -203,7 +202,7 @@ public void Add(ValidationResult item) throw new ArgumentNullException(nameof(item)); } - IEnumerable propertiesAffected = item.MemberNames; + IEnumerable propertiesAffected = item.MemberNames; // If there are no members affected, then force the entity-level // change event to occur. Otherwise, notifications will be raised @@ -211,7 +210,7 @@ public void Add(ValidationResult item) // include null/empty members for entity-level errors. if (!propertiesAffected.Any()) { - propertiesAffected = new string[] { null }; + propertiesAffected = [null]; } this._results.Add(item); @@ -237,7 +236,7 @@ public void Clear() this._results.Clear(); // There are no properties directly affected by this action, so use an empty enumerable - this.OnCollectionChanged(Enumerable.Empty()); + this.OnCollectionChanged([]); } } @@ -247,7 +246,7 @@ public void Clear() /// protected virtual void OnClear() { - if (this._parent == null || !this._parentMetaType.HasComplexMembers) + if (this._parent == null || !this._parentMetaType!.HasComplexMembers) { return; } @@ -325,7 +324,7 @@ public bool Remove(ValidationResult item) throw new ArgumentNullException(nameof(item)); } - IEnumerable propertiesAffected = item.MemberNames; + IEnumerable propertiesAffected = item.MemberNames; if (this._results.Remove(item)) { @@ -335,7 +334,7 @@ public bool Remove(ValidationResult item) // include null/empty members for entity-level errors. if (!propertiesAffected.Any()) { - propertiesAffected = new string[] { null }; + propertiesAffected = [null]; } this.OnRemove(item); @@ -348,7 +347,7 @@ public bool Remove(ValidationResult item) } protected virtual void OnRemove(ValidationResult item) - { + { } #endregion diff --git a/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs b/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs index dca8fc179..9445b1b63 100644 --- a/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs +++ b/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs @@ -91,13 +91,13 @@ internal static bool IsBindable(Type[] parameterTypes, object?[]? parameters) { if (parameters![i] == null) { - if (!TypeUtility.IsNullableType(parameterTypes[i]) - && parameterTypes[i].IsValueType) + if (parameterTypes[i].IsValueType + && !TypeUtility.IsNullableType(parameterTypes[i])) { return false; } } - else if (!parameterTypes[i].IsAssignableFrom(parameters[i].GetType())) + else if (!parameterTypes[i].IsAssignableFrom(parameters[i]!.GetType())) { return false; } @@ -751,6 +751,8 @@ private static string NormalizeMemberPath(string memberPath, Type memberType) #if !SERVERFX internal class ValidationResultEqualityComparer : EqualityComparer { + public static ValidationResultEqualityComparer Instance { get; } = new ValidationResultEqualityComparer(); + public override bool Equals(ValidationResult left, ValidationResult right) { if (left.ErrorMessage.Equals(right.ErrorMessage, StringComparison.Ordinal) && left.MemberNames.SequenceEqual(right.MemberNames)) From b536e853fe339aa5528ff677ba2d1f769754274b Mon Sep 17 00:00:00 2001 From: Daniel Svensson Date: Fri, 24 Apr 2026 22:53:05 +0200 Subject: [PATCH 19/19] fix coderabbit comments --- src/OpenRiaServices.Client/Framework/Entity.cs | 8 ++++---- src/OpenRiaServices.Client/Framework/Polyfills.cs | 7 ++++--- .../Framework/ValidationUtilities.cs | 2 +- .../Framework/Data/DomainService.cs | 12 ++++-------- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/OpenRiaServices.Client/Framework/Entity.cs b/src/OpenRiaServices.Client/Framework/Entity.cs index 175060482..f0efd10de 100644 --- a/src/OpenRiaServices.Client/Framework/Entity.cs +++ b/src/OpenRiaServices.Client/Framework/Entity.cs @@ -1971,20 +1971,20 @@ public static void ExtractState(this Entity targetEntity, IDictionary + /// [Obsolete("Use ApplyState to instead.")] public static void ExtractState(this Entity targetEntity, IDictionary entityStateToApply, LoadBehavior loadBehavior) { targetEntity.ApplyState(entityStateToApply, loadBehavior); } - /// + /// public static void ApplyState(this Entity targetEntity, IDictionary entityStateToApply) { targetEntity.ApplyState(entityStateToApply); } - /// + /// public static void ApplyState(this Entity targetEntity, IDictionary entityStateToApply, LoadBehavior loadBehavior) { targetEntity.ApplyState(entityStateToApply, loadBehavior); @@ -1996,7 +1996,7 @@ public static void UpdateOriginalValues(this Entity targetEntity, Entity entity) targetEntity.UpdateOriginalValues(entity); } - /// + /// public static void UpdateOriginalValues(this Entity targetEntity, IDictionary entityStateToApply) { targetEntity.UpdateOriginalValues(entityStateToApply); diff --git a/src/OpenRiaServices.Client/Framework/Polyfills.cs b/src/OpenRiaServices.Client/Framework/Polyfills.cs index d66303912..5826383ac 100644 --- a/src/OpenRiaServices.Client/Framework/Polyfills.cs +++ b/src/OpenRiaServices.Client/Framework/Polyfills.cs @@ -20,15 +20,16 @@ public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpres if (argument is null) ThrowArgumentNullException(paramName); } + } -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. + extension(ArgumentException) + { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) { - if (string.IsNullOrEmpty(argument)) + if (argument is null || argument.Length == 0) ThrowArgumentNullException(paramName); } -#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. } [DoesNotReturn] diff --git a/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs b/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs index 9445b1b63..0053e0979 100644 --- a/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs +++ b/src/OpenRiaServices.Client/Framework/ValidationUtilities.cs @@ -67,7 +67,7 @@ internal static MethodInfo GetMethod(object instance, string methodName, object? else { // convert parameter types into a string of this format e.g. ('string', null, 'int') - string[] parameterTypes = parameters!.Select(p => ((p == null) ? "null" : string.Format(CultureInfo.InvariantCulture, "'{0}'", p.GetType().ToString()))).ToArray(); + string[] parameterTypes = parameters!.Select(p => ((p == null) ? "null" : string.Format(CultureInfo.InvariantCulture, "'{0}'", p.GetType()))).ToArray(); throw new MissingMethodException(string.Format(CultureInfo.CurrentCulture, DataResource.ValidationUtilities_MethodNotFound, instanceType, methodName, parameterLength, string.Join(", ", parameterTypes))); } } diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs index 016251127..41f289a24 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs @@ -173,7 +173,7 @@ public AuthorizationResult IsAuthorized(DomainOperationEntry domainOperationEntr // Formulate an AuthorizationContext from the optional template provided by the user AuthorizationContext? contextTemplate = this.AuthorizationContext; - AuthorizationResult? result = null; + AuthorizationResult result; // If the developer specified a template, we will clone from it and use it as the IServiceProvider. // If the user did not, we create a new instance and use the ServiceContext as the IServiceProvider. @@ -575,18 +575,14 @@ public virtual async ValueTask SubmitAsync(ChangeSet changeSet, Cancellati /// private static AuthorizationResult EvaluateAuthorization(IEnumerable attributes, IPrincipal principal, AuthorizationContext authorizationContext) { - System.Diagnostics.Debug.Assert(attributes != null, "Authorization attributes cannot be null"); - System.Diagnostics.Debug.Assert(principal != null, "Principal cannot be null"); - System.Diagnostics.Debug.Assert(authorizationContext != null, "AuthorizationContext cannot be null"); - // 2 passes. // Pass 1 does [RequiresAuthentication] so we ensure it is always first. The idea is that if it is present, that is the most informative. // Pass 2 does the rest - foreach (AuthorizationAttribute attribute in attributes!) + foreach (AuthorizationAttribute attribute in attributes) { if (attribute is RequiresAuthenticationAttribute) { - AuthorizationResult result = attribute.Authorize(principal!, authorizationContext!); + AuthorizationResult result = attribute.Authorize(principal, authorizationContext!); if (result != AuthorizationResult.Allowed) { return result; @@ -600,7 +596,7 @@ private static AuthorizationResult EvaluateAuthorization(IEnumerable