Skip to content

Add additional Nullable annotations#569

Merged
Daniel-Svensson merged 19 commits into
OpenRIAServices:mainfrom
Daniel-Svensson:nullable2
Apr 24, 2026
Merged

Add additional Nullable annotations#569
Daniel-Svensson merged 19 commits into
OpenRIAServices:mainfrom
Daniel-Svensson:nullable2

Conversation

@Daniel-Svensson
Copy link
Copy Markdown
Member

@Daniel-Svensson Daniel-Svensson commented Apr 13, 2026

add more nullable annotations

Summary by CodeRabbit

  • Chores

    • Broad adoption of nullable reference annotations across client and server APIs for safer null handling.
    • Added runtime polyfills and improved changelog notes documenting nullability and modernization.
    • Strengthened argument validation and updated many APIs to accept/return optional/null values.
  • New Features

    • Entity state APIs extended to support nullable-aware state dictionaries and related overloads.
  • Tests

    • Added automatic retry to a flaky end-to-end test and improved related test behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds nullable reference types across many Client and Server source files, updating fields, properties, method signatures, events, and constructors to nullable-aware types; introduces small polyfills and nullable helper attributes; adjusts analyzer settings and a test to retry on failure.

Changes

Cohort / File(s) Summary
Editor Configuration
src/.editorconfig
Added analyzer setting for CA1826 (set to warning) and dotnet_code_quality.CA1826.exclude_ordefault_methods = true.
Polyfills & Attributes
src/OpenRiaServices.Client/Framework/Polyfills.cs
Added ThrowIfNull/ThrowIfNullOrEmpty helpers, caller/member-not-null/does-not-return attribute stubs, and made collection polyfills internal.
Client — Core operations & results
src/OpenRiaServices.Client/Framework/OperationBase.cs, .../InvokeArgs.cs, .../InvokeCompletedResult.cs, .../InvokeOperation.cs, .../InvokeResult.cs, .../LoadOperation.cs, .../LoadResult.cs, .../SubmitOperation.cs, .../SubmitResult.cs
Enabled nullable: many fields, constructor parameters, result/property types, callbacks and continuation handling made nullable or null-safe; some backing fields replaced with auto-properties.
Client — Entities & collections
src/OpenRiaServices.Client/Framework/Entity.cs, .../Entity.INotifyDataErrorInfo.cs, .../EntityRef.cs, .../IEntityCollection.cs, .../IEntityCollection\1.cs, .../EntityCollection.cs, .../EntitySet.cs, .../EntityCollectionChangedEventArgs.cs`
Enabled nullable: entity state, refs, identity, original-values APIs and many events adjusted to nullable-aware types; minor use of null-forgiving operators and event delegate nullability.
Client — Complex types & validation
src/OpenRiaServices.Client/Framework/ComplexObject.cs, .../ComplexObject.INotifyDataErrorInfo.cs, .../ValidationResultCollection.cs, .../ValidationUtilities.cs, .../ValidationResultCollection.cs
Enabled nullable: validation plumbing amended to accept nullable member names, comparer singleton added, many internals switched to nullable types.
Client — Actions & State utilities
src/OpenRiaServices.Client/Framework/EntityAction.cs, .../ObjectStateUtility.cs
Parameters and state dictionaries changed to nullable-aware types (object?[], IDictionary<string, object?>); constructors and helpers updated accordingly.
Client — Query & parameters
src/OpenRiaServices.Client/Framework/EntityQuery.cs
Made parameter storage and Query nullable (IDictionary<string, object?>?, IQueryable?); adjusted constructors and visibility.
Client — DomainContext & related
src/OpenRiaServices.Client/Framework/DomainContext.cs, .../DomainIdentifierAttribute.cs
Enabled nullable: many DomainContext APIs, events, and parameter dictionaries updated to nullable-aware types; reflection calls adjusted for nullability.
Server — Authorization & context
src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs, .../AuthorizationContext.cs, .../DomainServiceContext.cs
Enabled nullable: ErrorMessage/ResourceType and context fields/properties made nullable; GetService returns object?; service/context initialization and MemberNotNull annotations added.
Server — Domain operations & service
src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs, .../DomainService.cs, .../DomainServiceErrorInfo.cs, .../DomainOperationParameter.cs, .../IDomainServiceFactory.cs
Enabled nullable broadly; operation invocation signatures return ValueTask<object?>, many service/context properties accept/return nullable types, and null-guards added.
Server — Invoke results & hosting
src/OpenRiaServices.Server/Framework/Data/ServiceInvokeResult.cs, src/OpenRiaServices.Hosting.AspNetCore/.../DataContractRequestSerializer.cs
Made Result and ValidationErrors nullable; added MemberNotNullWhen on HasValidationErrors; used null-forgiving for operation.AssociatedType when constructing generic types.
Tests & Changelog
src/Test/.../UpdateTests.cs, src/Test/.../OperationTests.cs, Changelog.md
Added [Retry(3)] to a test; adjusted a test to pass a non-null SubmitResult in a callback path; Changelog expanded to document nullability and polyfills.
Misc small edits
assorted files (ILoadResult.cs, IDomainClientFactory.cs, EntityCollectionChangedEventArgs.cs, LoadResult.cs, etc.)
File-level #nullable enable directives and small null-forgiving/cast adjustments applied without changing runtime behavior or public APIs in most cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Area-Server-Hosting

Poem

🐰 Hopping through code with a curious twitch,

I dot each question mark, stitch by stitch.
Nulls now invited, but handled with care,
Polyfills tucked in, safety in the air.
A tiny rabbit cheers: type-safety everywhere!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding nullable reference type annotations throughout the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 83.43% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Daniel-Svensson Daniel-Svensson marked this pull request as ready for review April 21, 2026 13:23
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (13)
src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs (2)

196-212: 🧹 Nitpick | 🔵 Trivial

Extend MemberNotNull to cover _operationType and drop the !.

_operation and _operationType are always set together in Setup, which is what the comment on line 202 relies on. You can encode that invariant directly in the attribute so the compiler stops needing the null-forgiving operator, and future refactors can't accidentally desync the two fields without the compiler noticing.

♻️ Proposed fix
-                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;
 `#pragma` warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant
-        [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_operation))]
+        [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_operation), nameof(_operationType))]
 `#pragma` warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant
         private void EnsureNotTemplate()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs` around
lines 196 - 212, Update the MemberNotNull attribute on EnsureNotTemplate to
include both backing fields so the compiler knows _operation and _operationType
are non-null together: modify the attribute on EnsureNotTemplate to list
nameof(_operation) and nameof(_operationType), then remove the null-forgiving
operator in the OperationType getter (refer to OperationType, EnsureNotTemplate,
_operation and _operationType and the Setup method that sets them together).

103-113: ⚠️ Potential issue | 🟡 Minor

Null-check authorizationContext before passing it to this(...).

: this((IServiceProvider) authorizationContext) dereferences/evaluates authorizationContext before the null-check on line 106-109 runs. Under #nullable enable, the parameter is declared non-nullable so this is mostly a contract issue, but the existing explicit null-check then becomes unreachable-for-nullable-oblivious-callers only by accident of evaluation order. Consider either removing the now-redundant runtime null-check (trusting the non-nullable annotation) or using a helper that validates first — mixed signaling here is confusing. Minor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs` around
lines 103 - 113, The constructor AuthorizationContext(object? instance, string
operation, string operationType, AuthorizationContext authorizationContext)
currently calls this((IServiceProvider)authorizationContext) before performing a
null-check on authorizationContext, which is confusing; remove the redundant
runtime null-check (the parameter is non-nullable) so the code consistently
relies on the non-nullable contract, and keep the call to
this((IServiceProvider)authorizationContext) and the subsequent Setup call that
uses authorizationContext._items and the existing comment about preserving lazy
instantiation.
src/OpenRiaServices.Client/Framework/SubmitResult.cs (1)

17-28: ⚠️ Potential issue | 🟡 Minor

ChangeSet is declared non-nullable but the constructor does not validate the argument.

Under #nullable enable, ChangeSet advertises a non-null contract to consumers, yet new SubmitResult(null!) (or a null flowing in through a nullable-oblivious caller) will silently produce a SubmitResult whose ChangeSet violates its declared type. A ThrowIfNull on construction would make the contract enforceable at runtime as well.

🛡️ Proposed fix
 public SubmitResult(EntityChangeSet changeSet)
 {
+    ArgumentNullException.ThrowIfNull(changeSet);
     ChangeSet = changeSet;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/SubmitResult.cs` around lines 17 - 28,
The SubmitResult constructor allows a null EntityChangeSet even though the
ChangeSet property is non-nullable; update the SubmitResult(EntityChangeSet
changeSet) constructor to validate changeSet (e.g., throw new
ArgumentNullException(nameof(changeSet)) or call a ThrowIfNull helper) before
assigning to the ChangeSet property so the non-null contract is enforced at
runtime.
src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs (2)

158-164: ⚠️ Potential issue | 🟠 Major

Make OperationAttribute nullable or enforce the invariant.

Line 163 returns _operationAttribute after the field was changed to Attribute?. Since the default branch can leave it unset, this public non-null contract is now misleading and can surface nullable warnings or downstream NREs.

🛠️ Proposed nullable-contract fix
-        public Attribute OperationAttribute
+        public Attribute? OperationAttribute
         {
             get
             {
                 this.InitializeOperationAttribute();
                 return this._operationAttribute;
             }
         }

If callers require a non-null attribute, throw here instead of returning the nullable field.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs` around
lines 158 - 164, The OperationAttribute getter returns the field
_operationAttribute which was changed to Attribute?; update the contract by
either making the OperationAttribute property type nullable (Attribute?) or
enforce the invariant by throwing when _operationAttribute is null—call
InitializeOperationAttribute(), check this._operationAttribute, and if still
null throw an InvalidOperationException (or similar) so callers never receive a
null; reference: OperationAttribute property, InitializeOperationAttribute(),
and the _operationAttribute field.

371-400: ⚠️ Potential issue | 🟠 Major

Propagate the nullable AssociatedType contract to existing callers.

Line 371 makes AssociatedType nullable, but known call sites still use it as non-null: DomainServiceDescription.AddQueryMethod calls AddEntityType(method.AssociatedType), DomainServiceDescription.AddSubmitMethod assigns it to Type entityType, and DomainService stores it directly in context.Items. Guard those paths or preserve a non-null invariant for operations that require an associated entity type.

Example downstream pattern:

- Type entityType = method.AssociatedType;
+ Type entityType = method.AssociatedType
+     ?? throw new InvalidOperationException("Submit methods must have an associated entity type.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs` around
lines 371 - 400, AssociatedType was changed to be nullable but callers assume
non-null; update all call sites to handle null instead of forcing a Type.
Specifically, in DomainServiceDescription.AddQueryMethod avoid calling
AddEntityType(method.AssociatedType) when method.AssociatedType is null (guard
and skip or handle appropriately), in DomainServiceDescription.AddSubmitMethod
check method.AssociatedType for null before assigning to a local Type entityType
(or use a nullable Type? and branch accordingly), and in DomainService where the
AssociatedType is stored into context.Items only store it when non-null (or
store a nullable marker and handle it where read). Locate references to
AssociatedType on DomainOperationEntry and add null checks/guards around
AddEntityType, AddSubmitMethod assignments, and context.Items usage so the
nullable contract is respected.
src/OpenRiaServices.Client/Framework/InvokeArgs.cs (1)

21-52: ⚠️ Potential issue | 🟡 Minor

Allow nullable values inside the invoke parameter bag.

Line 21 allows the parameter dictionary itself to be null, but IDictionary<string, object> still says individual argument values cannot be null. Nullable invoke parameters need object? values.

🛠️ Proposed nullable-contract fix
-        public InvokeArgs(string operationName, Type returnType, IDictionary<string, object>? parameters, bool hasSideEffects)
+        public InvokeArgs(string operationName, Type returnType, IDictionary<string, object?>? parameters, bool hasSideEffects)
@@
-        public IDictionary<string, object>? Parameters { get; }
+        public IDictionary<string, object?>? Parameters { get; }

Apply the same value-nullability annotation to downstream parameter-bag consumers as needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/InvokeArgs.cs` around lines 21 - 52, The
Parameters bag currently uses IDictionary<string, object>? which disallows null
values for individual entries; change the Parameters type in the InvokeArgs
constructor signature and the Parameters property to IDictionary<string,
object?>? (i.e., allow object? values) so callers can pass nullable argument
values, update the constructor parameter name and assignment to match that
nullable type (constructor InvokeArgs(string operationName, Type returnType,
IDictionary<string, object?>? parameters, bool hasSideEffects) and the property
Parameters { get; }), and then search for and update any downstream
consumers/places accessing InvokeArgs.Parameters to handle possible null entry
values (add null checks, null-coalescing or pattern matches as appropriate).
src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs (1)

51-57: ⚠️ Potential issue | 🟡 Minor

Return both null and empty member-name errors for entity-level queries.

Line 56 only checks for the exact propertyName value, so GetErrors(null) misses entity-level errors stored with "", and GetErrors("") misses errors stored with null, despite the comment saying both should be included.

🐛 Proposed fix
-                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));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs` around
lines 51 - 57, The predicate in the GetErrors branch that handles entity-level
queries only checks e.MemberNames.Contains(propertyName) and therefore misses
errors recorded with the opposite null/empty member-name; update the condition
in the if (string.IsNullOrEmpty(propertyName)) block (the logic operating on
this.ValidationErrors inside GetErrors/Entity.INotifyDataErrorInfo) to include
member names that are null or empty as well as the queried propertyName—for
example, replace the current Where(...) with one that matches
e.MemberNames.Any(m => string.IsNullOrEmpty(m) || m == propertyName) or
equivalent so both "" and null stored member names are returned for entity-level
queries.
src/OpenRiaServices.Client/Framework/EntityQuery.cs (1)

188-189: 🛠️ Refactor suggestion | 🟠 Major

Propagate nullable parameters to the generic derived constructor too.

The base EntityQuery ctor now accepts IDictionary<string, object>?, but EntityQuery<TEntity>'s internal ctor still declares IDictionary<string, object> parameters (non-nullable). Callers that legitimately pass null (as supported by the base contract and XML docs) will now produce a nullable-analysis warning. For consistency, make this parameter nullable too.

♻️ Proposed change
-        internal EntityQuery(DomainClient domainClient, string queryName, IDictionary<string, object> parameters, bool hasSideEffects, bool isComposable)
+        internal EntityQuery(DomainClient domainClient, string queryName, IDictionary<string, object>? parameters, bool hasSideEffects, bool isComposable)
             : base(domainClient, queryName, typeof(TEntity), parameters, hasSideEffects, isComposable)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/EntityQuery.cs` around lines 188 - 189,
The generic constructor EntityQuery<TEntity>(DomainClient domainClient, string
queryName, IDictionary<string, object> parameters, bool hasSideEffects, bool
isComposable) should accept a nullable parameters type to match the base
EntityQuery constructor; change the parameters parameter to IDictionary<string,
object>? so callers can pass null without nullable warnings, and ensure the call
to base(domainClient, queryName, typeof(TEntity), parameters, hasSideEffects,
isComposable) continues to pass the (now nullable) parameter value unchanged.
src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs (1)

26-78: 🧹 Nitpick | 🔵 Trivial

LGTM — but note this is a binary/source-compat-sensitive public API change.

Making User, Operation, and GetService return types nullable, and relaxing the ctor user parameter to IPrincipal?, is a public surface change for server authors. Source-breaking for any override of GetService declared with non-nullable object return in consuming code compiled with nullable-warnings-as-errors. Consider mentioning this in the release notes / migration section of the PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs` around
lines 26 - 78, The public API was changed to nullable for
DomainServiceContext.User, DomainServiceContext.Operation,
DomainServiceContext.GetService return type and the constructor parameter
DomainServiceContext(IServiceProvider, IPrincipal? ...) which can break
source/binary compatibility for consumers (e.g., overrides of GetService
compiled with non-nullable returns); update the release notes and migration
guide to explicitly list these symbols (User, Operation, GetService, and the
nullable ctor overload) and provide guidance for consumers to recompile or add
nullable annotations/adjust overrides (or keep a non-nullable wrapper) to avoid
build errors under nullable-warnings-as-errors.
src/OpenRiaServices.Client/Framework/EntityRef.cs (1)

410-417: ⚠️ Potential issue | 🟠 Major

[MemberNotNullWhen(true, nameof(Entity))] on HasValue violates its contract.

IEntityRef.HasValue returns true whenever _entity != null, but the Entity getter (lines 80–117) can still return null even when HasValue is true. When Entity is accessed, if !_hasAssignedEntity && !_entityPredicate(_entity), the getter resets _entity = null (line 92) and attempts to load via GetSingleMatch, which may return null. Callers relying on this attribute contract (e.g., if (entityRef.HasValue) use(entityRef.Entity);) in EntitySet.cs:1160, ChangeSetBuilder.cs:407, and ChangeSetBuilder.cs:485 are vulnerable to null dereference.

Unlike OperationBase.HasError, which safely guarantees its contract with a simple null check, HasValue's promise is broken by the Entity getter's side effects. Either remove the [MemberNotNullWhen] attribute from both IEntityRef.HasValue (line 456) and the implementation (line 410), or tighten HasValue to return this.Entity != null (accepting the deferred load side effect).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/EntityRef.cs` around lines 410 - 417,
The MemberNotNullWhen attribute on IEntityRef.HasValue/EntityRef.HasValue is
incorrect because HasValue only checks _entity != null while the Entity getter
can null-out _entity and return null (due to _hasAssignedEntity,
_entityPredicate and GetSingleMatch), breaking the attribute contract; fix by
either removing the [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true,
nameof(Entity))] annotation from the interface member (IEntityRef.HasValue) and
the implementation (EntityRef.HasValue) or change the HasValue implementation to
force the same semantics as the Entity getter (return this.Entity != null) so
the attribute holds; update callers in EntitySet.cs and ChangeSetBuilder.cs as
needed to rely on the chosen behavior.
src/OpenRiaServices.Server/Framework/Data/DomainService.cs (3)

585-609: 🧹 Nitpick | 🔵 Trivial

Redundant null-forgiving operators.

attributes, principal, and authorizationContext are all declared as non-nullable parameters on EvaluateAuthorization, so attributes!, principal!, and authorizationContext! here are no-ops that only add noise.

♻️ Proposed cleanup
-            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);
@@
-                    AuthorizationResult result = attribute.Authorize(principal!, authorizationContext!);
+                    AuthorizationResult result = attribute.Authorize(principal, authorizationContext);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs` around lines 585
- 609, The null-forgiving operators are redundant: remove the unnecessary '!'
usages on attributes, principal, and authorizationContext in the
EvaluateAuthorization logic; update the two foreach blocks to iterate over
attributes (not attributes!) and call attribute.Authorize(principal,
authorizationContext) (not principal!/authorizationContext!), keeping the same
RequiresAuthenticationAttribute checks and return logic in
DomainService.EvaluateAuthorization.

1135-1185: ⚠️ Potential issue | 🔴 Critical

Wrong return value: boxing a ValueTask<object?> as the object? result.

InvokeDomainOperationEntryAsync is async ValueTask<object?>, so the body must return an object? value (the async state machine wraps it in a ValueTask<object?>). The fallthrough statement returns new ValueTask<object?>((object?)null) which gets boxed into object? and becomes the awaited result — callers that ever observe this return value would receive a boxed ValueTask<object?> instead of null.

Current callers (InvokeCudOperationsAsync, InvokeCustomOperations) discard the result so it isn't observably broken today, but the code is semantically wrong and will bite any future caller.

🐛 Proposed fix
-            return new ValueTask<object?>((object?)null);
+            return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs` around lines 1135
- 1185, The method InvokeDomainOperationEntryAsync incorrectly returns new
ValueTask<object?>((object?)null) at the end, which boxes a ValueTask into the
object result; change the final return to return null; so the async state
machine produces a ValueTask<object?> with a null result. Locate the method
InvokeDomainOperationEntryAsync and replace the fall-through return expression
with a plain null return (return null;) while leaving the existing exception
handling and ServiceContext cleanup intact.

175-209: 🧹 Nitpick | 🔵 Trivial

result should not be nullable.

EvaluateAuthorization returns a non-nullable AuthorizationResult and the using block unconditionally assigns to result. Declaring AuthorizationResult? result = null; makes the return at line 209 a potential CS8603 (nullable-to-non-nullable) and is semantically misleading — the final value is always non-null.

♻️ Proposed cleanup
-            AuthorizationContext? contextTemplate = this.AuthorizationContext;
-            AuthorizationResult? result = null;
+            AuthorizationContext? contextTemplate = this.AuthorizationContext;
+            AuthorizationResult result;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs` around lines 175
- 209, The local variable `AuthorizationResult? result = null;` is incorrectly
nullable; change it to a non-nullable `AuthorizationResult result;` (remove the
`?` and the `= null` initializer) because `DomainService.EvaluateAuthorization`
returns a non-null `AuthorizationResult` and the using block always assigns
`result` before it is returned; update any related nullable annotations if
necessary so the method returns a non-null `AuthorizationResult`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/OpenRiaServices.Client/Framework/Polyfills.cs`:
- Around line 13-30: The file uses C# 14 extension member syntax inside the
Polyfills class (the extension(ArgumentNullException) block and the ThrowIfNull
method) which won't compile under older language versions for
net472/netstandard2.0; update the consuming project files to set
<LangVersion>14.0</LangVersion> (or latest/preview) so the extension member
compiles, and remove the unused private ThrowArgumentNullException method
(ThrowArgumentNullException) from Polyfills to eliminate dead code.

In `@src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs`:
- Around line 158-161: The local variable `message` is declared as nullable but
`ErrorMessageAccessor` returns a non-nullable string; change the local from
`string? message` to `string message` so the call to
`string.Format(CultureInfo.CurrentCulture, message, operation)` uses a
non-nullable format string; update the declaration where
`ErrorMessageAccessor()` is invoked (refer to `ErrorMessageAccessor`, local
`message`, and the `string.Format(..., message, operation)` call).

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs`:
- Around line 661-671: Remove the superfluous null-forgiving initializer and
unnecessary null-forgiving call: declare customMethodOperation as
DomainOperationEntry? without initializing it (or initialize to null), drop the
trailing ! on domainServiceDescription.GetCustomMethod(...) so the
null-coalescing assignment reads naturally, and keep the existing comment about
ResolveOperations validating non-null; references: customMethodOperation,
invokedActions, domainServiceDescription.GetCustomMethod and ResolveOperations.

In `@src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs`:
- Line 21: Remove the explicit nameof(exception) when calling
ArgumentNullException.ThrowIfNull so the CallerArgumentExpression can supply the
parameter name (replace ArgumentNullException.ThrowIfNull(exception,
nameof(exception)) with a single-argument ThrowIfNull(exception)); also update
the Error property's setter (the setter around lines 37-40 in
DomainServiceErrorInfo) to use ArgumentNullException.ThrowIfNull(error) the same
way for consistency.

---

Outside diff comments:
In `@src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs`:
- Around line 51-57: The predicate in the GetErrors branch that handles
entity-level queries only checks e.MemberNames.Contains(propertyName) and
therefore misses errors recorded with the opposite null/empty member-name;
update the condition in the if (string.IsNullOrEmpty(propertyName)) block (the
logic operating on this.ValidationErrors inside
GetErrors/Entity.INotifyDataErrorInfo) to include member names that are null or
empty as well as the queried propertyName—for example, replace the current
Where(...) with one that matches e.MemberNames.Any(m => string.IsNullOrEmpty(m)
|| m == propertyName) or equivalent so both "" and null stored member names are
returned for entity-level queries.

In `@src/OpenRiaServices.Client/Framework/EntityQuery.cs`:
- Around line 188-189: The generic constructor EntityQuery<TEntity>(DomainClient
domainClient, string queryName, IDictionary<string, object> parameters, bool
hasSideEffects, bool isComposable) should accept a nullable parameters type to
match the base EntityQuery constructor; change the parameters parameter to
IDictionary<string, object>? so callers can pass null without nullable warnings,
and ensure the call to base(domainClient, queryName, typeof(TEntity),
parameters, hasSideEffects, isComposable) continues to pass the (now nullable)
parameter value unchanged.

In `@src/OpenRiaServices.Client/Framework/EntityRef.cs`:
- Around line 410-417: The MemberNotNullWhen attribute on
IEntityRef.HasValue/EntityRef.HasValue is incorrect because HasValue only checks
_entity != null while the Entity getter can null-out _entity and return null
(due to _hasAssignedEntity, _entityPredicate and GetSingleMatch), breaking the
attribute contract; fix by either removing the
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(Entity))]
annotation from the interface member (IEntityRef.HasValue) and the
implementation (EntityRef.HasValue) or change the HasValue implementation to
force the same semantics as the Entity getter (return this.Entity != null) so
the attribute holds; update callers in EntitySet.cs and ChangeSetBuilder.cs as
needed to rely on the chosen behavior.

In `@src/OpenRiaServices.Client/Framework/InvokeArgs.cs`:
- Around line 21-52: The Parameters bag currently uses IDictionary<string,
object>? which disallows null values for individual entries; change the
Parameters type in the InvokeArgs constructor signature and the Parameters
property to IDictionary<string, object?>? (i.e., allow object? values) so
callers can pass nullable argument values, update the constructor parameter name
and assignment to match that nullable type (constructor InvokeArgs(string
operationName, Type returnType, IDictionary<string, object?>? parameters, bool
hasSideEffects) and the property Parameters { get; }), and then search for and
update any downstream consumers/places accessing InvokeArgs.Parameters to handle
possible null entry values (add null checks, null-coalescing or pattern matches
as appropriate).

In `@src/OpenRiaServices.Client/Framework/SubmitResult.cs`:
- Around line 17-28: The SubmitResult constructor allows a null EntityChangeSet
even though the ChangeSet property is non-nullable; update the
SubmitResult(EntityChangeSet changeSet) constructor to validate changeSet (e.g.,
throw new ArgumentNullException(nameof(changeSet)) or call a ThrowIfNull helper)
before assigning to the ChangeSet property so the non-null contract is enforced
at runtime.

In `@src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs`:
- Around line 196-212: Update the MemberNotNull attribute on EnsureNotTemplate
to include both backing fields so the compiler knows _operation and
_operationType are non-null together: modify the attribute on EnsureNotTemplate
to list nameof(_operation) and nameof(_operationType), then remove the
null-forgiving operator in the OperationType getter (refer to OperationType,
EnsureNotTemplate, _operation and _operationType and the Setup method that sets
them together).
- Around line 103-113: The constructor AuthorizationContext(object? instance,
string operation, string operationType, AuthorizationContext
authorizationContext) currently calls
this((IServiceProvider)authorizationContext) before performing a null-check on
authorizationContext, which is confusing; remove the redundant runtime
null-check (the parameter is non-nullable) so the code consistently relies on
the non-nullable contract, and keep the call to
this((IServiceProvider)authorizationContext) and the subsequent Setup call that
uses authorizationContext._items and the existing comment about preserving lazy
instantiation.

In `@src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs`:
- Around line 158-164: The OperationAttribute getter returns the field
_operationAttribute which was changed to Attribute?; update the contract by
either making the OperationAttribute property type nullable (Attribute?) or
enforce the invariant by throwing when _operationAttribute is null—call
InitializeOperationAttribute(), check this._operationAttribute, and if still
null throw an InvalidOperationException (or similar) so callers never receive a
null; reference: OperationAttribute property, InitializeOperationAttribute(),
and the _operationAttribute field.
- Around line 371-400: AssociatedType was changed to be nullable but callers
assume non-null; update all call sites to handle null instead of forcing a Type.
Specifically, in DomainServiceDescription.AddQueryMethod avoid calling
AddEntityType(method.AssociatedType) when method.AssociatedType is null (guard
and skip or handle appropriately), in DomainServiceDescription.AddSubmitMethod
check method.AssociatedType for null before assigning to a local Type entityType
(or use a nullable Type? and branch accordingly), and in DomainService where the
AssociatedType is stored into context.Items only store it when non-null (or
store a nullable marker and handle it where read). Locate references to
AssociatedType on DomainOperationEntry and add null checks/guards around
AddEntityType, AddSubmitMethod assignments, and context.Items usage so the
nullable contract is respected.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs`:
- Around line 585-609: The null-forgiving operators are redundant: remove the
unnecessary '!' usages on attributes, principal, and authorizationContext in the
EvaluateAuthorization logic; update the two foreach blocks to iterate over
attributes (not attributes!) and call attribute.Authorize(principal,
authorizationContext) (not principal!/authorizationContext!), keeping the same
RequiresAuthenticationAttribute checks and return logic in
DomainService.EvaluateAuthorization.
- Around line 1135-1185: The method InvokeDomainOperationEntryAsync incorrectly
returns new ValueTask<object?>((object?)null) at the end, which boxes a
ValueTask into the object result; change the final return to return null; so the
async state machine produces a ValueTask<object?> with a null result. Locate the
method InvokeDomainOperationEntryAsync and replace the fall-through return
expression with a plain null return (return null;) while leaving the existing
exception handling and ServiceContext cleanup intact.
- Around line 175-209: The local variable `AuthorizationResult? result = null;`
is incorrectly nullable; change it to a non-nullable `AuthorizationResult
result;` (remove the `?` and the `= null` initializer) because
`DomainService.EvaluateAuthorization` returns a non-null `AuthorizationResult`
and the using block always assigns `result` before it is returned; update any
related nullable annotations if necessary so the method returns a non-null
`AuthorizationResult`.

In `@src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs`:
- Around line 26-78: The public API was changed to nullable for
DomainServiceContext.User, DomainServiceContext.Operation,
DomainServiceContext.GetService return type and the constructor parameter
DomainServiceContext(IServiceProvider, IPrincipal? ...) which can break
source/binary compatibility for consumers (e.g., overrides of GetService
compiled with non-nullable returns); update the release notes and migration
guide to explicitly list these symbols (User, Operation, GetService, and the
nullable ctor overload) and provide guidance for consumers to recompile or add
nullable annotations/adjust overrides (or keep a non-nullable wrapper) to avoid
build errors under nullable-warnings-as-errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: babb1169-a9d8-4ef7-ae02-11f59a5db901

📥 Commits

Reviewing files that changed from the base of the PR and between d08e158 and 092ce7b.

📒 Files selected for processing (26)
  • src/.editorconfig
  • src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs
  • src/OpenRiaServices.Client/Framework/EntityQuery.cs
  • src/OpenRiaServices.Client/Framework/EntityRef.cs
  • src/OpenRiaServices.Client/Framework/IDomainClientFactory.cs
  • src/OpenRiaServices.Client/Framework/IEntityCollection.cs
  • src/OpenRiaServices.Client/Framework/IEntityCollection1.cs`
  • src/OpenRiaServices.Client/Framework/InvokeArgs.cs
  • src/OpenRiaServices.Client/Framework/InvokeCompletedResult.cs
  • src/OpenRiaServices.Client/Framework/InvokeOperation.cs
  • src/OpenRiaServices.Client/Framework/LoadOperation.cs
  • src/OpenRiaServices.Client/Framework/LoadResult.cs
  • src/OpenRiaServices.Client/Framework/OperationBase.cs
  • src/OpenRiaServices.Client/Framework/Polyfills.cs
  • src/OpenRiaServices.Client/Framework/SubmitOperation.cs
  • src/OpenRiaServices.Client/Framework/SubmitResult.cs
  • src/OpenRiaServices.Client/Framework/ValidationUtilities.cs
  • src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs
  • src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainOperationParameter.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainService.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs
  • src/OpenRiaServices.Server/Framework/Data/IDomainServiceFactory.cs
  • src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/UpdateTests.cs

Comment thread src/OpenRiaServices.Client/Framework/Polyfills.cs
Comment thread src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs Outdated
Comment thread src/OpenRiaServices.Server/Framework/Data/DomainService.cs Outdated
Comment thread src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs Outdated
@Daniel-Svensson Daniel-Svensson changed the title Nullable2 Add additional Nullable annotations Apr 21, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs (1)

158-165: ⚠️ Potential issue | 🟡 Minor

OperationAttribute getter can return null despite the non-nullable return type.

_operationAttribute is now Attribute? (line 22). InitializeOperationAttribute has a default: branch (line 298) that intentionally does nothing when Operation is DomainOperation.None (per the comment about ClassifyDomainOperation), so this getter can leak a null through a property typed as non-nullable Attribute. That should either warn (CS8603) or, if warnings-as-errors are disabled, silently produce a contract violation for consumers using nullable context.

🛡️ Options

Either throw when accessed before the operation is classified:

 public Attribute OperationAttribute
 {
     get
     {
         this.InitializeOperationAttribute();
-        return this._operationAttribute;
+        return this._operationAttribute
+            ?? throw new InvalidOperationException("OperationAttribute accessed before DomainOperation was classified.");
     }
 }

…or make the property nullable (Attribute?) to reflect actual behavior (note: this is a public-API change, so may need a changelog entry).

The two _operationAttribute! usages at lines 307/311 are fine because they are only reached when attributeCreated == true.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs` around
lines 158 - 165, The OperationAttribute getter currently calls
InitializeOperationAttribute() but can return a null _operationAttribute (now
Attribute?) when Operation is DomainOperation.None; fix by either making the
property return Attribute? (change OperationAttribute's return type to nullable)
or enforce non-null by throwing a meaningful InvalidOperationException when
_operationAttribute is null (e.g., after InitializeOperationAttribute() check
_operationAttribute and throw with context), and adjust any callers/tests
accordingly; reference the OperationAttribute property, the _operationAttribute
field, and InitializeOperationAttribute (which has the default branch for
DomainOperation.None) when making the change.
src/OpenRiaServices.Server/Framework/Data/DomainService.cs (1)

576-612: 🧹 Nitpick | 🔵 Trivial

Redundant null-forgiving operators on already-non-nullable parameters.

attributes, principal, and authorizationContext are all declared non-nullable on the method signature (and are Debug.Assert'd as non-null on lines 578-580). The ! suffixes at lines 585, 589, and 603 don't change semantics and just add noise from the nullable conversion pass.

♻️ Proposed cleanup
-            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);
@@
-                    AuthorizationResult result = attribute.Authorize(principal!, authorizationContext!);
+                    AuthorizationResult result = attribute.Authorize(principal, authorizationContext);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs` around lines 576
- 612, In EvaluateAuthorization, remove the redundant null-forgiving operators
used on the already non-nullable parameters (attributes!, principal!,
authorizationContext!) — locate the two foreach loops and the calls to
attribute.Authorize and drop the trailing '!' from attributes, principal, and
authorizationContext so the code uses the declared non-nullable variables
directly (e.g., change attribute.Authorize(principal!, authorizationContext!) to
attribute.Authorize(principal, authorizationContext) and similarly remove '!' on
attributes in the foreach headers).
♻️ Duplicate comments (1)
src/OpenRiaServices.Client/Framework/Polyfills.cs (1)

1-38: ⚠️ Potential issue | 🟠 Major

Make these shims shared across non-NET targets and verify the required LangVersion.

These polyfills live under the Client project, but Server files in this PR also call ArgumentNullException.ThrowIfNull and use attributes like MemberNotNull; non-NET Server builds need equivalent shims compiled into the Server assembly. Also, the extension(ArgumentNullException) block and collection expressions require a sufficiently new C# language version; the LangVersion portion matches the earlier unresolved review concern.

#!/bin/bash
# Description: Verify whether all non-NET projects using new BCL/nullability APIs
# have the required shared shims and a language version that supports this syntax.
# Expected:
# 1. Any non-NET Server project using ThrowIfNull/MemberNotNull has local/shared polyfill sources.
# 2. Projects compiling this file use LangVersion 14.0/latest/preview or otherwise support extension blocks.

echo "Project target frameworks and language versions:"
fd -e csproj | xargs rg -nP -C2 '<(TargetFrameworks?|LangVersion)>'

echo
echo "Polyfills.cs locations:"
fd -i '^Polyfills\.cs$'

echo
echo "Server/Client usage of APIs that require non-NET shims:"
rg -nP -C2 'ArgumentNullException\.ThrowIfNull|ArgumentNullException\.ThrowIfNullOrEmpty|\bMemberNotNull(When)?\b|\bNotNull\b|\bDoesNotReturn\b' \
  src/OpenRiaServices.Server src/OpenRiaServices.Client --glob '*.cs'

Also applies to: 179-246

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/Polyfills.cs` around lines 1 - 38,
Polyfills defined in Polyfills.cs (the extension(ArgumentNullException) block
implementing ThrowIfNull and ThrowIfNullOrEmpty and the
ThrowArgumentNullException helper) must be available to all non-.NET builds that
use these APIs (including Server), so either move this file into a shared
compilation item (Shared project or Shared folder included by both Client and
Server) or add an equivalent Polyfills.cs to the Server assembly; also ensure
projects compiling this syntax set LangVersion to a C# version that supports
extension blocks/collection expressions (e.g., latest/preview or LangVersion
14.0) and that the Server project references System.Diagnostics.CodeAnalysis
types (or includes the necessary attributes like
MemberNotNull/NotNull/DoesNotReturn) so the attributes used in Polyfills compile
correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Changelog.md`:
- Around line 17-22: Add a blank line immediately after the "### Server" heading
in Changelog.md (ensure there's an empty line between "### Server" and the
following list item line "* Added Nullability annotations for a few core types
of public API"); optionally also ensure there's a blank line above the "###
Server" heading to match spacing used for other top-level sections for
consistency.

In `@src/OpenRiaServices.Client/Framework/EntityQuery.cs`:
- Around line 188-189: The EntityQuery constructor currently declares parameters
as non-nullable; change its signature to accept a nullable parameter dictionary
so it matches the base constructor and call patterns: modify the
EntityQuery(DomainClient domainClient, string queryName, IDictionary<string,
object?> parameters, bool hasSideEffects, bool isComposable) constructor to use
IDictionary<string, object?>? parameters and pass that nullable value through to
base(..., parameters, ...), ensuring the nullable contract is preserved for
no-parameter queries.

In `@src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs`:
- Line 359: Update the two override method signatures to match the changed
abstract return type: change ReflectionDomainOperationEntry.InvokeAsync and
TestDomainOperationEntry.InvokeAsync to return ValueTask<object?> instead of
ValueTask<object>, and update any return expressions or local variables as
needed to allow returning null (or cast to object?) to satisfy nullable
reference types; keep the method bodies and parameters unchanged aside from the
return type so they correctly override InvokeAsync in DomainOperationEntry.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs`:
- Line 1183: Replace the explicit allocated ValueTask containing a null with the
idiomatic default ValueTask to avoid unnecessary allocation: change the
expression "new ValueTask<object?>((object?)null)" to
"default(ValueTask<object?>)". Locate the return statement that currently
returns the allocated ValueTask<object?> and update it to return
default(ValueTask<object?>) (or simply default) so the method returns a
synchronously-completed null ValueTask without allocation.
- Line 189: Replace the redundant null-check expression when obtaining the
principal by directly assigning IPrincipal? principal = this.ServiceContext.User
(remove the unnecessary "this.ServiceContext != null ? ... : null" ternary),
while leaving the subsequent defensive check "if (principal == null)" in place
to cover the rare obsolete-constructor case; refer to the ServiceContext
property and the DomainServiceContext type to locate where EnsureInitialized
guarantees non-null ServiceContext and to update the assignment in
DomainService.cs.

In `@src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs`:
- Around line 40-43: The constructor DomainServiceContext(IServiceProvider
serviceProvider, DomainOperationType operationType) currently uses
serviceProvider.GetService(typeof(IPrincipal))! which can return null and then
assigns a null to the non-nullable User property; update this constructor to
explicitly resolve the principal from serviceProvider, check for null, and throw
a clear ArgumentException/InvalidOperationException (e.g. "IPrincipal service
not registered") if it is null so User is never populated with null;
alternatively, if you prefer nullable semantics, change the User property to
IPrincipal? and adjust consumers such as DomainService.IsAuthorized to handle
nulls—but prefer the explicit null-check + throw in the DomainServiceContext
constructor to maintain the non-nullable contract.

---

Outside diff comments:
In `@src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs`:
- Around line 158-165: The OperationAttribute getter currently calls
InitializeOperationAttribute() but can return a null _operationAttribute (now
Attribute?) when Operation is DomainOperation.None; fix by either making the
property return Attribute? (change OperationAttribute's return type to nullable)
or enforce non-null by throwing a meaningful InvalidOperationException when
_operationAttribute is null (e.g., after InitializeOperationAttribute() check
_operationAttribute and throw with context), and adjust any callers/tests
accordingly; reference the OperationAttribute property, the _operationAttribute
field, and InitializeOperationAttribute (which has the default branch for
DomainOperation.None) when making the change.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs`:
- Around line 576-612: In EvaluateAuthorization, remove the redundant
null-forgiving operators used on the already non-nullable parameters
(attributes!, principal!, authorizationContext!) — locate the two foreach loops
and the calls to attribute.Authorize and drop the trailing '!' from attributes,
principal, and authorizationContext so the code uses the declared non-nullable
variables directly (e.g., change attribute.Authorize(principal!,
authorizationContext!) to attribute.Authorize(principal, authorizationContext)
and similarly remove '!' on attributes in the foreach headers).

---

Duplicate comments:
In `@src/OpenRiaServices.Client/Framework/Polyfills.cs`:
- Around line 1-38: Polyfills defined in Polyfills.cs (the
extension(ArgumentNullException) block implementing ThrowIfNull and
ThrowIfNullOrEmpty and the ThrowArgumentNullException helper) must be available
to all non-.NET builds that use these APIs (including Server), so either move
this file into a shared compilation item (Shared project or Shared folder
included by both Client and Server) or add an equivalent Polyfills.cs to the
Server assembly; also ensure projects compiling this syntax set LangVersion to a
C# version that supports extension blocks/collection expressions (e.g.,
latest/preview or LangVersion 14.0) and that the Server project references
System.Diagnostics.CodeAnalysis types (or includes the necessary attributes like
MemberNotNull/NotNull/DoesNotReturn) so the attributes used in Polyfills compile
correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 51862947-cbee-40cc-a5cf-418fda2903fc

📥 Commits

Reviewing files that changed from the base of the PR and between 092ce7b and df7d917.

📒 Files selected for processing (15)
  • Changelog.md
  • src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs
  • src/OpenRiaServices.Client/Framework/EntityQuery.cs
  • src/OpenRiaServices.Client/Framework/EntityRef.cs
  • src/OpenRiaServices.Client/Framework/InvokeArgs.cs
  • src/OpenRiaServices.Client/Framework/Polyfills.cs
  • src/OpenRiaServices.Client/Framework/SubmitResult.cs
  • src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractRequestSerializer.cs
  • src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs
  • src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainService.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs
  • src/OpenRiaServices.Server/Framework/Data/ServiceInvokeResult.cs

Comment thread Changelog.md
Comment thread src/OpenRiaServices.Client/Framework/EntityQuery.cs Outdated
Comment thread src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs
Comment thread src/OpenRiaServices.Server/Framework/Data/DomainService.cs Outdated
Comment thread src/OpenRiaServices.Server/Framework/Data/DomainService.cs
Comment thread src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Changelog.md`:
- Around line 17-18: Consolidate the duplicated changelog bullets by removing
one of the two overlapping lines and leaving a single clear bullet that covers
the change; for example keep a single entry mentioning "Nullability annotations
for EntitySet and EntityCollection" (and optionally combine with "Nullability
annotations for many parts of the most used public API") so the note references
EntitySet, EntityCollection and the public API without duplication.

In `@src/OpenRiaServices.Client/Framework/Entity.cs`:
- Around line 1963-1973: The two extension methods named ExtractState should be
renamed to ApplyState so their public API matches their behavior and XML
inheritdoc; change the method names for the overloads "public static void
ExtractState(this Entity targetEntity, IDictionary<string, object>
entityStateToApply)" and "public static void ExtractState(this Entity
targetEntity, IDictionary<string, object> entityStateToApply, LoadBehavior
loadBehavior)" to "ApplyState" (keeping signatures and bodies unchanged) so they
correctly act as overloads of Entity.ApplyState and preserve the inherited docs
referencing ApplyState.

In `@src/OpenRiaServices.Client/Framework/EntityQuery.cs`:
- Around line 198-199: The XML documentation for the internal property
EntityQuery.Query uses a malformed <inheritdoc> tag; change it to a self-closing
tag (e.g., <inheritdoc />) so the doc comment is well-formed and the inherited
documentation is picked up by tooling—update the XML comment immediately above
the internal new IQueryable<TEntity>? Query property (symbol: EntityQuery.Query)
to use a properly closed <inheritdoc /> tag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bce0b230-748b-4669-bee1-a28ff5d96f4c

📥 Commits

Reviewing files that changed from the base of the PR and between df7d917 and 15c17be.

📒 Files selected for processing (5)
  • Changelog.md
  • src/OpenRiaServices.Client/Framework/Entity.cs
  • src/OpenRiaServices.Client/Framework/EntityQuery.cs
  • src/OpenRiaServices.Client/Framework/InvokeOperation.cs
  • src/OpenRiaServices.Client/Framework/InvokeResult.cs

Comment thread Changelog.md
Comment thread src/OpenRiaServices.Client/Framework/Entity.cs Outdated
Comment thread src/OpenRiaServices.Client/Framework/EntityQuery.cs Outdated
@Daniel-Svensson Daniel-Svensson enabled auto-merge (squash) April 21, 2026 20:44
@Daniel-Svensson Daniel-Svensson added this to the 5.8.0 milestone Apr 21, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/OpenRiaServices.Server/Framework/Data/DomainService.cs (2)

175-209: 🧹 Nitpick | 🔵 Trivial

Optional: tighten result typing.

result is declared AuthorizationResult? and initialized to null (line 176), but it is unconditionally assigned at line 203 before being returned at line 209. The nullable annotation is not needed; declaring it non-nullable (and moving it into the using block) more accurately reflects the control flow and removes a needless null initialization.

♻️ Proposed cleanup
             AuthorizationContext? contextTemplate = this.AuthorizationContext;
-            AuthorizationResult? result = null;
+            AuthorizationResult result;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs` around lines 175
- 209, Declare AuthorizationResult as non-nullable and scope it inside the using
block where it is assigned: remove the nullable initialization
AuthorizationResult? result = null at the top, and instead declare
AuthorizationResult result (non-nullable) within the using (AuthorizationContext
context ...) block just before the first assignment from
DomainService.EvaluateAuthorization; this eliminates the unnecessary null state
and keeps the lifetime of result local to the AuthorizationContext scope while
preserving the existing calls to
DomainService.EvaluateAuthorization(typeLevelAttributes, principal, context) and
the subsequent conditional reassignment.

585-603: 🧹 Nitpick | 🔵 Trivial

Drop the redundant null-forgiving operators on non-nullable parameters.

EvaluateAuthorization declares attributes, principal, and authorizationContext as non-nullable (line 576), and the caller at lines 203/206 also passes non-nullable values (principal is null-checked at 194, context comes from a using initializer). The ! suppressions at lines 585, 589, and 603 are unnecessary noise and actively misleading — they imply a nullability concern that does not exist in the signature.

♻️ Proposed cleanup
-            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);
@@
             foreach (AuthorizationAttribute attribute in attributes)
             {
                 if (!(attribute is RequiresAuthenticationAttribute))
                 {
-                    AuthorizationResult result = attribute.Authorize(principal!, authorizationContext!);
+                    AuthorizationResult result = attribute.Authorize(principal, authorizationContext);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs` around lines 585
- 603, Remove the redundant null-forgiving operators used on non-nullable
parameters in EvaluateAuthorization: drop the "!" suffixes on attributes,
principal, and authorizationContext in the foreach loops and Authorize calls so
the code uses the declared non-nullable variables directly (refer to the
EvaluateAuthorization method and the variables attributes, principal,
authorizationContext and the AuthorizationAttribute.Authorize calls).
src/OpenRiaServices.Client/Framework/Entity.cs (1)

280-311: ⚠️ Potential issue | 🟡 Minor

Possible null dereference on LastSet after nullable enablement.

LastSet now returns EntitySet?, but line 288 dereferences it directly as this.LastSet.EntityContainer.HasChildChanges(this). Although in practice a tracked entity should be attached, flow analysis cannot prove that here and this should produce a CS8602 warning under #nullable enable. Contrast with line 310, which correctly uses ?. on EntitySet. Please either add a null check, ! suppression with a brief comment explaining the invariant, or switch to this.LastSet?.EntityContainer.HasChildChanges(this) ?? false.

🛡️ Suggested fix
-                this._hasChildChanges = this.LastSet.EntityContainer.HasChildChanges(this);
+                // _trackChanges is only true once attached, so LastSet must be non-null here.
+                this._hasChildChanges = this.LastSet!.EntityContainer.HasChildChanges(this);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/Entity.cs` around lines 280 - 311,
OnChildUpdate currently dereferences this.LastSet (which is now nullable) when
calling LastSet.EntityContainer.HasChildChanges(this), causing a possible null
dereference; update the method to guard that call by either (a) checking LastSet
!= null before accessing EntityContainer, (b) using the null-conditional pattern
this.LastSet?.EntityContainer.HasChildChanges(this) ?? false, or (c) using a
null-forgiving operator with a short comment asserting the invariant (e.g., //
LastSet is non-null for tracked entities) — apply the change around the
HasChildChanges call in OnChildUpdate and keep the existing safe
null-conditional usage for EntitySet.TrackAsInteresting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/OpenRiaServices.Client/Framework/Entity.cs`:
- Around line 1967-1979: The Obsolete attribute messages on the two ExtractState
extension methods are grammatically wrong; update both [Obsolete("Use ApplyState
to instead.")] occurrences on the ExtractState(this Entity, IDictionary<string,
object?>) and ExtractState(this Entity, IDictionary<string, object?>,
LoadBehavior) methods to read "Use ApplyState instead." so the compiler warning
shows the corrected wording when consumers see the deprecation message.
- Around line 1961-2015: The XML doc cref tags on the extension wrappers
reference non-nullable IDictionary signatures; update the cref values to match
the actual nullable signatures (IDictionary{string, object?}) for the affected
members: change the cref on the ApplyState(this Entity, IDictionary<string,
object?>, LoadBehavior) wrapper currently pointing to
Entity.ApplyState(IDictionary{string, object}, LoadBehavior) to
Entity.ApplyState(IDictionary{string, object?}, LoadBehavior), change the cref
on the ApplyState(this Entity, IDictionary<string, object?>) wrapper to
Entity.ApplyState(IDictionary{string, object?}), change the cref on the other
ApplyState(this Entity, IDictionary<string, object?>, LoadBehavior) overload to
Entity.ApplyState(IDictionary{string, object?}, LoadBehavior), and change the
cref on UpdateOriginalValues(this Entity, IDictionary<string, object?>) to
Entity.UpdateOriginalValues(IDictionary{string, object?}); ensure the
curly-brace generic cref format matches the other docs exactly.
- Around line 1546-1548: The comment and assumption around
MetaType.GetEntityAction returning non-null are incorrect; update the code in
the block where EntityActionAttribute customUpdate =
MetaType.GetEntityAction(action.Name) is used (and the similar call at line
~1880) to either (1) add a null check and guard usage of customUpdate (e.g.,
only call RaisePropertyChanged(customUpdate.CanInvokePropertyName) when
customUpdate != null, or (2) change the comment to state that
MetaType.GetEntityAction may return null and that a fallback/dummy
EntityActionAttribute may exist from the CustomMethodInvocation setter), so
ensure references to customUpdate.CanInvokePropertyName are only used when
customUpdate is non-null or explicitly handled; update the comment text
accordingly to reflect this invariant change.

In `@src/OpenRiaServices.Client/Framework/EntityQuery.cs`:
- Around line 198-199: Remove the duplicate XML doc inheritdoc tag for the Query
member: in the EntityQuery class where the member documentation contains two
identical lines <inheritdoc cref="EntityQuery.Query" />, delete the redundant
line so only one <inheritdoc cref="EntityQuery.Query" /> remains (or replace the
duplicate with the intended distinct summary if a custom comment was meant).
Ensure the documentation for EntityQuery.Query contains a single, correct XML
comment tag.

In `@src/OpenRiaServices.Client/Framework/ILoadResult.cs`:
- Around line 3-4: The LoadResult<TEntity> constructor must validate
non-nullable inputs: add guard clauses in the LoadResult(EntityQuery<TEntity>
query, LoadBehavior loadBehavior, IEnumerable<TEntity> entities,
IReadOnlyCollection<Entity> allEntities, int totalEntityCount) constructor to
throw ArgumentNullException for both allEntities and entities before using them;
then perform the existing conversion of entities into _loadedEntites (using
Data.ReadOnlyObservableLoaderCollection<TEntity> or ToList()) and assign
EntityQuery, LoadBehavior, AllEntities and TotalEntityCount as before so
Entities/AllEntities can never be null at runtime.

In `@src/OpenRiaServices.Client/Framework/Polyfills.cs`:
- Around line 24-31: The current polyfill defines ThrowIfNullOrEmpty as an
extension on ArgumentNullException; move it to an extension on ArgumentException
(i.e., declare the method in an extension(ArgumentException) block) so callsites
using ArgumentException.ThrowIfNullOrEmpty(...) compile like the BCL; update the
implementation of ThrowIfNullOrEmpty(string? argument, string? paramName = null)
to throw ArgumentNullException when argument is null and ArgumentException when
argument is empty (preserving CallerArgumentExpression usage for paramName), and
keep the aggressive inlining and nullable annotations unchanged.

In `@src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs`:
- Around line 380-382: Capture the change set once and reuse it instead of
calling cities.EntityContainer.GetChanges() twice: call
cities.EntityContainer.GetChanges() into a local variable (e.g., changeSet) and
pass that same changeSet to the SubmitOperation constructor (where
submitOperation is created) and to the new SubmitResult(...) used in
AssertExceptionIsCorrectlyRaisedOnSyncContext; this ensures submitTaskSource,
SubmitOperation, and SubmitResult all reference the exact same EntityChangeSet
instance.

---

Outside diff comments:
In `@src/OpenRiaServices.Client/Framework/Entity.cs`:
- Around line 280-311: OnChildUpdate currently dereferences this.LastSet (which
is now nullable) when calling LastSet.EntityContainer.HasChildChanges(this),
causing a possible null dereference; update the method to guard that call by
either (a) checking LastSet != null before accessing EntityContainer, (b) using
the null-conditional pattern this.LastSet?.EntityContainer.HasChildChanges(this)
?? false, or (c) using a null-forgiving operator with a short comment asserting
the invariant (e.g., // LastSet is non-null for tracked entities) — apply the
change around the HasChildChanges call in OnChildUpdate and keep the existing
safe null-conditional usage for EntitySet.TrackAsInteresting.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs`:
- Around line 175-209: Declare AuthorizationResult as non-nullable and scope it
inside the using block where it is assigned: remove the nullable initialization
AuthorizationResult? result = null at the top, and instead declare
AuthorizationResult result (non-nullable) within the using (AuthorizationContext
context ...) block just before the first assignment from
DomainService.EvaluateAuthorization; this eliminates the unnecessary null state
and keeps the lifetime of result local to the AuthorizationContext scope while
preserving the existing calls to
DomainService.EvaluateAuthorization(typeLevelAttributes, principal, context) and
the subsequent conditional reassignment.
- Around line 585-603: Remove the redundant null-forgiving operators used on
non-nullable parameters in EvaluateAuthorization: drop the "!" suffixes on
attributes, principal, and authorizationContext in the foreach loops and
Authorize calls so the code uses the declared non-nullable variables directly
(refer to the EvaluateAuthorization method and the variables attributes,
principal, authorizationContext and the AuthorizationAttribute.Authorize calls).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6e7b0fb5-935b-429e-8bb7-7e823a54177e

📥 Commits

Reviewing files that changed from the base of the PR and between 15c17be and a70bc43.

📒 Files selected for processing (15)
  • src/OpenRiaServices.Client/Framework/ComplexObject.INotifyDataErrorInfo.cs
  • src/OpenRiaServices.Client/Framework/ComplexObject.cs
  • src/OpenRiaServices.Client/Framework/Entity.cs
  • src/OpenRiaServices.Client/Framework/EntityAction.cs
  • src/OpenRiaServices.Client/Framework/EntityCollection.cs
  • src/OpenRiaServices.Client/Framework/EntityCollectionChangedEventArgs.cs
  • src/OpenRiaServices.Client/Framework/EntityQuery.cs
  • src/OpenRiaServices.Client/Framework/EntitySet.cs
  • src/OpenRiaServices.Client/Framework/ILoadResult.cs
  • src/OpenRiaServices.Client/Framework/ObjectStateUtility.cs
  • src/OpenRiaServices.Client/Framework/Polyfills.cs
  • src/OpenRiaServices.Client/Framework/SubmitResult.cs
  • src/OpenRiaServices.Client/Framework/ValidationUtilities.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainService.cs
  • src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs

Comment thread src/OpenRiaServices.Client/Framework/Entity.cs
Comment thread src/OpenRiaServices.Client/Framework/Entity.cs
Comment thread src/OpenRiaServices.Client/Framework/Entity.cs
Comment thread src/OpenRiaServices.Client/Framework/EntityQuery.cs
Comment thread src/OpenRiaServices.Client/Framework/ILoadResult.cs
Comment thread src/OpenRiaServices.Client/Framework/Polyfills.cs Outdated
Comment thread src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs
Comment thread src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs Dismissed
Comment thread src/OpenRiaServices.Client/Framework/ValidationUtilities.cs Fixed
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs (1)

158-165: ⚠️ Potential issue | 🟡 Minor

OperationAttribute public return type should be nullable to match the backing field.

_operationAttribute is declared Attribute? and the default: branch of InitializeOperationAttribute (the DomainOperation.None/pre-classification path) intentionally leaves it unset. The property getter on Line 163 therefore can return null, but its declared return type is the non-nullable Attribute — either a ! silencer or a declared Attribute? would reflect reality more accurately.

Since this is part of the public API, changing the signature is technically a nullable-contract change; if you'd prefer to keep the existing non-null contract, consider either (a) asserting non-null via _operationAttribute! with a debug guard, or (b) marking the property Attribute? now that nullable is enabled on this file.

Also applies to: 298-301

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs` around
lines 158 - 165, The public getter OperationAttribute can return null because it
reads the backing field _operationAttribute which is declared Attribute? and
InitializeOperationAttribute may leave it unset; update the public contract by
changing the OperationAttribute property's return type to Attribute? (nullable)
so it accurately reflects possible nulls, and do the same for the other public
property in this file that returns an Attribute and reads the same backing
field; alternatively, if you must preserve a non-null contract, explicitly
assert non-null with _operationAttribute! and add a defensive debug guard or
exception in InitializeOperationAttribute to guarantee non-null before
returning.
src/OpenRiaServices.Server/Framework/Data/DomainService.cs (2)

585-599: 🧹 Nitpick | 🔵 Trivial

Redundant null-forgiving on authorizationContext.

EvaluateAuthorization's authorizationContext parameter is declared as non-nullable AuthorizationContext, so authorizationContext! at lines 585 and 599 adds no value. Drop the !.

♻️ Proposed cleanup
-                    AuthorizationResult result = attribute.Authorize(principal, authorizationContext!);
+                    AuthorizationResult result = attribute.Authorize(principal, authorizationContext);

(apply in both locations)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs` around lines 585
- 599, In EvaluateAuthorization, remove the unnecessary null-forgiving operator
from the AuthorizationContext parameter when calling
AuthorizationAttribute.Authorize: replace calls using authorizationContext! with
authorizationContext (both occurrences where AuthorizationResult result =
attribute.Authorize(principal, authorizationContext!) is invoked) so the
non-nullable AuthorizationContext parameter is used directly in the
AuthorizationAttribute.Authorize calls.

733-759: 🧹 Nitpick | 🔵 Trivial

Consolidate the repeated this.ChangeSet!. pattern.

ChangeSet is legitimately non-null inside AuthorizeChangeSet, ValidateChangeSetAsync, ExecuteChangeSetAsync, PersistChangeSetAsyncInternal, InvokeCudOperationsAsync, InvokeCustomOperations, and ResolveOperations because SubmitAsync assigns it before any of them runs. Rather than sprinkling ! eight times across the file, consider a private helper (e.g., private ChangeSet CurrentChangeSet => _changeSet ?? throw new InvalidOperationException(...);) or a [MemberNotNull(nameof(_changeSet))] helper analogous to EnsureInitialized. This keeps the nullable contract explicit and centralised, and also guards against a developer inadvertently invoking these methods outside the submit pipeline.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs` around lines 733
- 759, Consolidate the repeated nullable-force usage of this.ChangeSet! by
adding a centralized non-null accessor and using it across methods: introduce a
private property or helper (e.g., CurrentChangeSet or EnsureChangeSetInitialized
with [MemberNotNull(nameof(_changeSet))]) that throws InvalidOperationException
if _changeSet is null, and replace all occurrences of this.ChangeSet! in
AuthorizeChangeSet, ValidateChangeSetAsync, ExecuteChangeSetAsync,
PersistChangeSetAsyncInternal, InvokeCudOperationsAsync, InvokeCustomOperations,
and ResolveOperations with the new accessor; ensure SubmitAsync still assigns
_changeSet before calling these methods and update any existing
EnsureInitialized usage to match the new pattern.
src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs (1)

51-62: ⚠️ Potential issue | 🟡 Minor

Filter logic now diverges from ComplexObject.INotifyDataErrorInfo.GetErrors.

Entity's null/empty branch was changed to e.MemberNames.Any(string.IsNullOrEmpty), but the parallel implementation in src/OpenRiaServices.Client/Framework/ComplexObject.INotifyDataErrorInfo.cs (line 55) still uses e.MemberNames.Contains(propertyName). For identical validation error shapes, the two types will now return different sets (e.g., a result whose MemberNames contains "" would be returned by Entity.GetErrors(null) but not by ComplexObject.GetErrors(null), and vice-versa). Either update ComplexObject to use the same predicate, or revert this one for consistency.

🔧 Suggested change to ComplexObject.INotifyDataErrorInfo.cs to align
-                results = this.ValidationResultCollection.Where(e => !e.MemberNames.Any() || e.MemberNames.Contains(propertyName));
+                results = this.ValidationResultCollection.Where(e => !e.MemberNames.Any() || e.MemberNames.Any(string.IsNullOrEmpty));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs` around
lines 51 - 62, The Entity INofityDataErrorInfo GetErrors implementation diverges
from ComplexObject.INotifyDataErrorInfo: Entity uses
e.MemberNames.Any(string.IsNullOrEmpty) for the null/empty propertyName branch
while ComplexObject still uses e.MemberNames.Contains(propertyName); make them
consistent by updating the ComplexObject.INotifyDataErrorInfo.GetErrors (or
revert the Entity logic) so both use the same predicate for the empty
propertyName case—specifically replace the ComplexObject null/empty branch with
the same check (e.MemberNames.Any() || e.MemberNames.Any(string.IsNullOrEmpty))
or change Entity back to the original Contains-based behavior so GetErrors
returns identical sets across both types.
♻️ Duplicate comments (1)
src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs (1)

380-382: 🧹 Nitpick | 🔵 Trivial

Consider capturing the changeset once here too, for consistency.

The same pattern was applied at Lines 357–358 (capture changeSet once, reuse). Here at Lines 381–382, cities.EntityContainer.GetChanges() is still called twice, producing two distinct EntityChangeSet instances passed to the SubmitOperation and to the asserted SubmitResult. Aligning with the earlier fix avoids any future confusion about whether the operation and its completion result reference the same change set.

♻️ Suggested tweak
             var submitTaskSource = new TaskCompletionSource<SubmitResult>(TaskCreationOptions.RunContinuationsAsynchronously);
-            var submitOperation = new SubmitOperation(cities.EntityContainer.GetChanges(), soCallback, null, submitTaskSource.Task, null);
-            await AssertExceptionIsCorrectlyRaisedOnSyncContext(submitOperation, submitTaskSource, new SubmitResult(cities.EntityContainer.GetChanges()));
+            var submitChangeSet = cities.EntityContainer.GetChanges();
+            var submitOperation = new SubmitOperation(submitChangeSet, soCallback, null, submitTaskSource.Task, null);
+            await AssertExceptionIsCorrectlyRaisedOnSyncContext(submitOperation, submitTaskSource, new SubmitResult(submitChangeSet));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs` around
lines 380 - 382, Capture the EntityChangeSet once into a local variable (e.g.,
changeSet) and reuse it for both constructing the SubmitOperation and the
expected SubmitResult so both reference the identical change set; specifically,
replace the two calls to cities.EntityContainer.GetChanges() with a single var
changeSet = cities.EntityContainer.GetChanges() and pass changeSet into the
SubmitOperation constructor (submitOperation) and into new SubmitResult.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Changelog.md`:
- Line 30: Update the changelog bullet that currently reads "Fix MSTEST****:
varius test related" by replacing the placeholder "MSTEST****" with the actual
analyzer/rule ID(s) being fixed (e.g., MSTESTxxxx or the appropriate rule
identifiers) and correct the typo "varius" to "various", so the line becomes
something like "Fix MSTEST1234: various test related" (use the real rule IDs and
keep the phrasing consistent with other bullets).

In `@src/OpenRiaServices.Client/Framework/DomainContext.cs`:
- Around line 900-909: The null-forgiving operator on results.ReturnValue is
misleading because the target cast is already TValue?; remove the unnecessary
'!' so the code returns new InvokeResult<TValue>((TValue?)results.ReturnValue);
if the real contract guarantees a non-null return when
results.ValidationErrors.Count == 0 and TValue must be non-null, instead change
the cast to the non-nullable form and assert non-null explicitly: new
InvokeResult<TValue>((TValue)results.ReturnValue!) — update the code in the
InvokeResult<TValue> return site (and related call sites in
DomainContext.InvokeOperation) to reflect the chosen contract.

In `@src/OpenRiaServices.Client/Framework/Entity.cs`:
- Around line 1471-1473: Replace the use of CultureInfo.InvariantCulture when
formatting the localized resource message in the Entity action lookup: in the
code path that calls MetaType.GetEntityAction(actionName) and throws new
InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
Resource.Entity_NoEntityActionWithName, actionName)), switch to using
CultureInfo.CurrentCulture so the Resource.Entity_NoEntityActionWithName message
is formatted with the current UI culture (keep the same exception type and
message parameters, only change the CultureInfo argument).

In `@src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/UpdateTests.cs`:
- Line 2315: Add the same explanatory trailing comment to the [Retry(3)]
attribute in UpdateTests so its rationale matches the other occurrences; locate
the [Retry(3)] attribute in UpdateTests (the flaky test annotation) and append
the comment used elsewhere in this file (e.g. // Known flaky test, retry until
it is rewritten) so intent is discoverable and consistent.

---

Outside diff comments:
In `@src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs`:
- Around line 51-62: The Entity INofityDataErrorInfo GetErrors implementation
diverges from ComplexObject.INotifyDataErrorInfo: Entity uses
e.MemberNames.Any(string.IsNullOrEmpty) for the null/empty propertyName branch
while ComplexObject still uses e.MemberNames.Contains(propertyName); make them
consistent by updating the ComplexObject.INotifyDataErrorInfo.GetErrors (or
revert the Entity logic) so both use the same predicate for the empty
propertyName case—specifically replace the ComplexObject null/empty branch with
the same check (e.MemberNames.Any() || e.MemberNames.Any(string.IsNullOrEmpty))
or change Entity back to the original Contains-based behavior so GetErrors
returns identical sets across both types.

In `@src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs`:
- Around line 158-165: The public getter OperationAttribute can return null
because it reads the backing field _operationAttribute which is declared
Attribute? and InitializeOperationAttribute may leave it unset; update the
public contract by changing the OperationAttribute property's return type to
Attribute? (nullable) so it accurately reflects possible nulls, and do the same
for the other public property in this file that returns an Attribute and reads
the same backing field; alternatively, if you must preserve a non-null contract,
explicitly assert non-null with _operationAttribute! and add a defensive debug
guard or exception in InitializeOperationAttribute to guarantee non-null before
returning.

In `@src/OpenRiaServices.Server/Framework/Data/DomainService.cs`:
- Around line 585-599: In EvaluateAuthorization, remove the unnecessary
null-forgiving operator from the AuthorizationContext parameter when calling
AuthorizationAttribute.Authorize: replace calls using authorizationContext! with
authorizationContext (both occurrences where AuthorizationResult result =
attribute.Authorize(principal, authorizationContext!) is invoked) so the
non-nullable AuthorizationContext parameter is used directly in the
AuthorizationAttribute.Authorize calls.
- Around line 733-759: Consolidate the repeated nullable-force usage of
this.ChangeSet! by adding a centralized non-null accessor and using it across
methods: introduce a private property or helper (e.g., CurrentChangeSet or
EnsureChangeSetInitialized with [MemberNotNull(nameof(_changeSet))]) that throws
InvalidOperationException if _changeSet is null, and replace all occurrences of
this.ChangeSet! in AuthorizeChangeSet, ValidateChangeSetAsync,
ExecuteChangeSetAsync, PersistChangeSetAsyncInternal, InvokeCudOperationsAsync,
InvokeCustomOperations, and ResolveOperations with the new accessor; ensure
SubmitAsync still assigns _changeSet before calling these methods and update any
existing EnsureInitialized usage to match the new pattern.

---

Duplicate comments:
In `@src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs`:
- Around line 380-382: Capture the EntityChangeSet once into a local variable
(e.g., changeSet) and reuse it for both constructing the SubmitOperation and the
expected SubmitResult so both reference the identical change set; specifically,
replace the two calls to cities.EntityContainer.GetChanges() with a single var
changeSet = cities.EntityContainer.GetChanges() and pass changeSet into the
SubmitOperation constructor (submitOperation) and into new SubmitResult.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c2786b3d-9e41-46b4-89f0-75f6d19ad6e4

📥 Commits

Reviewing files that changed from the base of the PR and between a70bc43 and b536e85.

📒 Files selected for processing (44)
  • Changelog.md
  • src/.editorconfig
  • src/OpenRiaServices.Client/Framework/ComplexObject.INotifyDataErrorInfo.cs
  • src/OpenRiaServices.Client/Framework/ComplexObject.cs
  • src/OpenRiaServices.Client/Framework/DomainContext.cs
  • src/OpenRiaServices.Client/Framework/DomainIdentifierAttribute.cs
  • src/OpenRiaServices.Client/Framework/Entity.INotifyDataErrorInfo.cs
  • src/OpenRiaServices.Client/Framework/Entity.cs
  • src/OpenRiaServices.Client/Framework/EntityAction.cs
  • src/OpenRiaServices.Client/Framework/EntityChangeSet.cs
  • src/OpenRiaServices.Client/Framework/EntityCollection.cs
  • src/OpenRiaServices.Client/Framework/EntityCollectionChangedEventArgs.cs
  • src/OpenRiaServices.Client/Framework/EntityQuery.cs
  • src/OpenRiaServices.Client/Framework/EntityRef.cs
  • src/OpenRiaServices.Client/Framework/EntitySet.cs
  • src/OpenRiaServices.Client/Framework/IDomainClientFactory.cs
  • src/OpenRiaServices.Client/Framework/IEntityCollection.cs
  • src/OpenRiaServices.Client/Framework/IEntityCollection1.cs`
  • src/OpenRiaServices.Client/Framework/ILoadResult.cs
  • src/OpenRiaServices.Client/Framework/InvokeArgs.cs
  • src/OpenRiaServices.Client/Framework/InvokeCompletedResult.cs
  • src/OpenRiaServices.Client/Framework/InvokeOperation.cs
  • src/OpenRiaServices.Client/Framework/InvokeResult.cs
  • src/OpenRiaServices.Client/Framework/LoadOperation.cs
  • src/OpenRiaServices.Client/Framework/LoadResult.cs
  • src/OpenRiaServices.Client/Framework/ObjectStateUtility.cs
  • src/OpenRiaServices.Client/Framework/OperationBase.cs
  • src/OpenRiaServices.Client/Framework/Polyfills.cs
  • src/OpenRiaServices.Client/Framework/SubmitOperation.cs
  • src/OpenRiaServices.Client/Framework/SubmitResult.cs
  • src/OpenRiaServices.Client/Framework/ValidationResultCollection.cs
  • src/OpenRiaServices.Client/Framework/ValidationUtilities.cs
  • src/OpenRiaServices.Hosting.AspNetCore/Framework/AspNetCore/Serialization/DataContractRequestSerializer.cs
  • src/OpenRiaServices.Server/Framework/Data/AuthorizationAttribute.cs
  • src/OpenRiaServices.Server/Framework/Data/AuthorizationContext.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainOperationEntry.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainOperationParameter.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainService.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainServiceContext.cs
  • src/OpenRiaServices.Server/Framework/Data/DomainServiceErrorInfo.cs
  • src/OpenRiaServices.Server/Framework/Data/IDomainServiceFactory.cs
  • src/OpenRiaServices.Server/Framework/Data/ServiceInvokeResult.cs
  • src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/OperationTests.cs
  • src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/UpdateTests.cs

Comment thread Changelog.md
Comment thread src/OpenRiaServices.Client/Framework/DomainContext.cs
Comment thread src/OpenRiaServices.Client/Framework/Entity.cs
Comment thread src/Test/OpenRiaservices.EndToEnd.Wcf.Test/Data/UpdateTests.cs
@sonarqubecloud
Copy link
Copy Markdown

@Daniel-Svensson Daniel-Svensson merged commit cbb540b into OpenRIAServices:main Apr 24, 2026
7 checks passed
@Daniel-Svensson Daniel-Svensson deleted the nullable2 branch April 24, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants