From 21fc62512f34d21da385b70cdcde11ae480c113d Mon Sep 17 00:00:00 2001 From: Jack Webb Date: Mon, 11 May 2026 10:20:00 +0200 Subject: [PATCH 01/10] Ensure ChangeSet is always initialized before use Removed nullable ChangeSet, added EnsureChangeSetInitialized() to throw if uninitialized, and replaced all null-forgiving usages. Added DomainService_ChangeSetNotInitialized resource string used in the thrown exception. --- .../Framework/Data/DomainService.cs | 32 +++++++++++++------ .../Framework/Data/Resource.Designer.cs | 11 ++++++- .../Framework/Data/Resource.resx | 3 ++ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs index dac892e2..d0f46df2 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs @@ -218,10 +218,11 @@ public void Dispose() /// /// Gets the current . Returns null if no change operations are being performed. /// - protected ChangeSet? ChangeSet + protected ChangeSet ChangeSet { get { + EnsureChangeSetInitialized(); return this._changeSet; } } @@ -718,7 +719,7 @@ protected virtual void Dispose(bool disposing) /// True if the is authorized, false otherwise. protected virtual bool AuthorizeChangeSet() { - foreach (ChangeSetEntry op in this.ChangeSet!.ChangeSetEntries) + foreach (ChangeSetEntry op in this.ChangeSet.ChangeSetEntries) { object entity = op.Entity; @@ -751,7 +752,7 @@ protected virtual bool AuthorizeChangeSet() protected virtual ValueTask ValidateChangeSetAsync(CancellationToken cancellationToken) { // Perform validation on the each of the operations. - var result = ValidateOperations(this.ChangeSet!.ChangeSetEntries, this.ServiceDescription, this.ValidationContext); + var result = ValidateOperations(this.ChangeSet.ChangeSetEntries, this.ServiceDescription, this.ValidationContext); return new ValueTask(result); } @@ -766,7 +767,7 @@ await this.InvokeCudOperationsAsync() await this.InvokeCustomOperations() .ConfigureAwait(false); - return !this.ChangeSet!.HasError; + return !this.ChangeSet.HasError; } /// @@ -794,7 +795,7 @@ protected virtual void OnError(DomainServiceErrorInfo errorInfo) { } private void ResolveOperations() { // Resolve and set the DomainOperationEntry for each operation in the changeset - foreach (ChangeSetEntry changeSetEntry in this.ChangeSet!.ChangeSetEntries) + foreach (ChangeSetEntry changeSetEntry in this.ChangeSet.ChangeSetEntries) { // resolve the DomainOperationEntry Type entityType = changeSetEntry.Entity.GetType(); @@ -984,7 +985,7 @@ private async Task PersistChangeSetAsyncInternal(CancellationToken cancell if (e.Value != null && e.ValidationResult != null) { IEnumerable updateOperations = - this.ChangeSet!.ChangeSetEntries.Where( + this.ChangeSet.ChangeSetEntries.Where( p => p.Operation == DomainOperation.Insert || p.Operation == DomainOperation.Update || p.Operation == DomainOperation.Delete); @@ -1003,7 +1004,7 @@ private async Task PersistChangeSetAsyncInternal(CancellationToken cancell } } - return !this.ChangeSet!.HasError; + return !this.ChangeSet.HasError; } /// @@ -1019,6 +1020,19 @@ private void EnsureInitialized() } } + /// + /// Ensures the has been initialized properly. + /// + /// if the instance hasn't been initialized. + [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_changeSet))] + private void EnsureChangeSetInitialized() + { + if (this._changeSet == null) + { + throw new InvalidOperationException(Resource.DomainService_ChangeSetNotInitialized); + } + } + /// /// Validate the permissions for the specified . If the authorization check /// fails, an will be thrown. @@ -1040,7 +1054,7 @@ private void ValidateMethodPermissions(DomainOperationEntry domainOperationEntry private async Task InvokeCudOperationsAsync() { object[] parameters = new object[1]; - foreach (ChangeSetEntry operation in this.ChangeSet!.ChangeSetEntries + foreach (ChangeSetEntry operation in this.ChangeSet.ChangeSetEntries .Where(op => op.Operation == DomainOperation.Insert || op.Operation == DomainOperation.Update || op.Operation == DomainOperation.Delete)) @@ -1082,7 +1096,7 @@ await this.InvokeDomainOperationEntryAsync(operation.DomainOperationEntry, param /// private async Task InvokeCustomOperations() { - foreach (ChangeSetEntry operation in this.ChangeSet!.ChangeSetEntries.Where(op => op.EntityActions != null && op.EntityActions.Any())) + foreach (ChangeSetEntry operation in this.ChangeSet.ChangeSetEntries.Where(op => op.EntityActions != null && op.EntityActions.Any())) { foreach (var entityAction in operation.EntityActions) { diff --git a/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs b/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs index ab5d7cc4..384b5a49 100644 --- a/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs +++ b/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs @@ -19,7 +19,7 @@ namespace OpenRiaServices.Server { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resource { @@ -231,6 +231,15 @@ internal static string DomainService_AlreadyInitialized { } } + /// + /// Looks up a localized string similar to ChangeSet has not been initialized.. + /// + internal static string DomainService_ChangeSetNotInitialized { + get { + return ResourceManager.GetString("DomainService_ChangeSetNotInitialized", resourceCulture); + } + } + /// /// Looks up a localized string similar to The domain operation entry named '{0}' provides redundant functionality. Another method named '{1}' already exists.. /// diff --git a/src/OpenRiaServices.Server/Framework/Data/Resource.resx b/src/OpenRiaServices.Server/Framework/Data/Resource.resx index 3fb2869d..4b1c11e7 100644 --- a/src/OpenRiaServices.Server/Framework/Data/Resource.resx +++ b/src/OpenRiaServices.Server/Framework/Data/Resource.resx @@ -213,6 +213,9 @@ This DomainService has already been initialized. + + ChangeSet has not been initialized. + The domain operation entry named '{0}' provides redundant functionality. Another method named '{1}' already exists. From 2a9d3dd379b694fd47f9a8b1df19b7e98cb2e760 Mon Sep 17 00:00:00 2001 From: Jack Webb Date: Mon, 11 May 2026 10:58:45 +0200 Subject: [PATCH 02/10] Add tests for ChangeSet accessibility in DomainService Added ChangeSetAccessorDomainService and related tests to verify ChangeSet accessibility and exception handling before and during initialization and submit operations. Implemented helper methods and assertions for test coverage. --- .../Test/DomainServiceTests.cs | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/OpenRiaServices.Server/Test/DomainServiceTests.cs b/src/OpenRiaServices.Server/Test/DomainServiceTests.cs index c75e076e..5c23dd92 100644 --- a/src/OpenRiaServices.Server/Test/DomainServiceTests.cs +++ b/src/OpenRiaServices.Server/Test/DomainServiceTests.cs @@ -48,6 +48,60 @@ public void TestInitialize() System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; } + /// + /// Verify that accessing ChangeSet before SubmitAsync throws InvalidOperationException. + /// + [TestMethod] + public void ChangeSet_ThrowsBeforeInitialization() + { + var ds = new ChangeSetAccessorDomainService(); + DomainServiceContext dsc = new DomainServiceContext(new MockDataService(), new MockUser("mathew"), DomainOperationType.Submit); + ds.Initialize(dsc); + + ExceptionHelper.ExpectInvalidOperationException( + () => { var _ = ds.GetChangeSet(); }, + Resource.DomainService_ChangeSetNotInitialized); + } + + /// + /// Verify that accessing ChangeSet before Initialize and SubmitAsync throws InvalidOperationException. + /// + [TestMethod] + public void ChangeSet_ThrowsBeforeInitializationAndSubmit() + { + var ds = new ChangeSetAccessorDomainService(); + + ExceptionHelper.ExpectInvalidOperationException( + () => { var _ = ds.GetChangeSet(); }, + Resource.DomainService_ChangeSetNotInitialized); + } + + /// + /// Verify that ChangeSet is accessible during SubmitAsync after initialization. + /// + [TestMethod] + public async Task ChangeSet_AccessibleAfterSubmitAsync() + { + var ds = new ChangeSetAccessorDomainService(); + DomainServiceContext dsc = new DomainServiceContext(new MockDataService(), new MockUser("mathew"), DomainOperationType.Submit); + ds.Initialize(dsc); + + City city = new City + { + Name = "TestCity", + CountyName = "TestCounty", + StateName = "WA" + }; + + ChangeSetEntry insertEntry = new ChangeSetEntry(1, city, null, DomainOperation.Insert); + ChangeSet cs = new ChangeSet(new[] { insertEntry }); + + bool result = await ds.SubmitAsync(cs, CancellationToken.None); + + Assert.IsTrue(result); + Assert.IsTrue(ds.ChangeSetWasAccessibleDuringSubmit, "ChangeSet should be accessible during SubmitAsync."); + } + /// /// Verify that both DAL providers support accessing their respective /// contexts in the constructor. @@ -2479,5 +2533,25 @@ public class DomainServiceInsertCustom_Validated_Object [CustomValidation(typeof(AlwaysFailValidator), "Validate")] public int IntProp { get; set; } } + public class ChangeSetAccessorDomainService : DomainService + { + public bool ChangeSetWasAccessibleDuringSubmit { get; private set; } + + public ChangeSet GetChangeSet() => ChangeSet; + + [Query] + public IQueryable GetCities() => Array.Empty().AsQueryable(); + + public void InsertCity(City city) + { + } + + protected override ValueTask PersistChangeSetAsync(CancellationToken cancellationToken) + { + // Verify ChangeSet is accessible during the submit pipeline + ChangeSetWasAccessibleDuringSubmit = ChangeSet != null; + return new ValueTask(true); + } + } #endregion // Mock DomainService Types } From 9c28050cd73bf6c1360736d64781970afe0bc99f Mon Sep 17 00:00:00 2001 From: Jack Webb Date: Tue, 12 May 2026 10:34:25 +0200 Subject: [PATCH 03/10] Update templates to NETFramework 4.8 --- .../AuthenticationDomainService.csproj | 4 ++-- .../CSharp/DomainServiceClass/DomainServiceClass.csproj | 4 ++-- .../BusinessApplicationProjectTemplate.csproj | 4 ++-- .../CSharp/RIAServicesLibrary/OpenRiaServicesLibrary.csproj | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/VisualStudio/ItemTemplates/CSharp/AuthenticationDomainService/AuthenticationDomainService.csproj b/src/VisualStudio/ItemTemplates/CSharp/AuthenticationDomainService/AuthenticationDomainService.csproj index cd128081..863a951a 100644 --- a/src/VisualStudio/ItemTemplates/CSharp/AuthenticationDomainService/AuthenticationDomainService.csproj +++ b/src/VisualStudio/ItemTemplates/CSharp/AuthenticationDomainService/AuthenticationDomainService.csproj @@ -31,7 +31,7 @@ Properties AuthenticationDomainService AuthenticationDomainService - v4.6 + v4.8 512 false false @@ -104,4 +104,4 @@ --> - \ No newline at end of file + diff --git a/src/VisualStudio/ItemTemplates/CSharp/DomainServiceClass/DomainServiceClass.csproj b/src/VisualStudio/ItemTemplates/CSharp/DomainServiceClass/DomainServiceClass.csproj index ebfd84a2..be974928 100644 --- a/src/VisualStudio/ItemTemplates/CSharp/DomainServiceClass/DomainServiceClass.csproj +++ b/src/VisualStudio/ItemTemplates/CSharp/DomainServiceClass/DomainServiceClass.csproj @@ -31,7 +31,7 @@ Properties DomainServiceClass DomainServiceClass - v4.6 + v4.8 512 false false @@ -103,4 +103,4 @@ --> - \ No newline at end of file + diff --git a/src/VisualStudio/Templates/CSharp/BusinessApplication/BusinessApplicationProjectTemplate.csproj b/src/VisualStudio/Templates/CSharp/BusinessApplication/BusinessApplicationProjectTemplate.csproj index e196b361..572fed78 100644 --- a/src/VisualStudio/Templates/CSharp/BusinessApplication/BusinessApplicationProjectTemplate.csproj +++ b/src/VisualStudio/Templates/CSharp/BusinessApplication/BusinessApplicationProjectTemplate.csproj @@ -15,7 +15,7 @@ Properties BusinessApplicationProjectTemplate BusinessApplicationProjectTemplate - v4.6 + v4.8 512 false false @@ -183,4 +183,4 @@ --> - \ No newline at end of file + diff --git a/src/VisualStudio/Templates/CSharp/RIAServicesLibrary/OpenRiaServicesLibrary.csproj b/src/VisualStudio/Templates/CSharp/RIAServicesLibrary/OpenRiaServicesLibrary.csproj index eb7c2915..4881ed5a 100644 --- a/src/VisualStudio/Templates/CSharp/RIAServicesLibrary/OpenRiaServicesLibrary.csproj +++ b/src/VisualStudio/Templates/CSharp/RIAServicesLibrary/OpenRiaServicesLibrary.csproj @@ -15,7 +15,7 @@ Properties OpenRiaServicesLibrary OpenRiaServicesLibrary - v4.6 + v4.8 512 false false @@ -76,4 +76,4 @@ --> - \ No newline at end of file + From 69c9ef554221aabf2c831b8da29af0d77cdfef29 Mon Sep 17 00:00:00 2001 From: Jack Webb Date: Tue, 12 May 2026 10:35:05 +0200 Subject: [PATCH 04/10] Inline exception in ChangeSet getter and improve the resource string --- .../Framework/Data/DomainService.cs | 26 ++++++++----------- .../Framework/Data/Resource.Designer.cs | 2 +- .../Framework/Data/Resource.resx | 2 +- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs index d0f46df2..5af87003 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; @@ -216,14 +217,22 @@ public void Dispose() } /// - /// Gets the current . Returns null if no change operations are being performed. + /// Gets the current . Throws if no change operations are being performed. /// + /// Thrown if no change operations are being performed. protected ChangeSet ChangeSet { get { - EnsureChangeSetInitialized(); + if (this._changeSet == null) + ThrowChangeSetNotInitialized(this.ServiceContext.OperationType); + return this._changeSet; + + [DoesNotReturn] + [System.Diagnostics.StackTraceHidden] + static void ThrowChangeSetNotInitialized(DomainOperationType serviceOperationType) => + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.DomainService_ChangeSetNotInitialized, serviceOperationType)); } } @@ -1020,19 +1029,6 @@ private void EnsureInitialized() } } - /// - /// Ensures the has been initialized properly. - /// - /// if the instance hasn't been initialized. - [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_changeSet))] - private void EnsureChangeSetInitialized() - { - if (this._changeSet == null) - { - throw new InvalidOperationException(Resource.DomainService_ChangeSetNotInitialized); - } - } - /// /// Validate the permissions for the specified . If the authorization check /// fails, an will be thrown. diff --git a/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs b/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs index 384b5a49..c77e812f 100644 --- a/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs +++ b/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs @@ -232,7 +232,7 @@ internal static string DomainService_AlreadyInitialized { } /// - /// Looks up a localized string similar to ChangeSet has not been initialized.. + /// Looks up a localized string similar to The ChangeSet of this DomainService is only initialized during a 'Submit' operation, so it cannot be accessed during an operation of type '{0}'.. /// internal static string DomainService_ChangeSetNotInitialized { get { diff --git a/src/OpenRiaServices.Server/Framework/Data/Resource.resx b/src/OpenRiaServices.Server/Framework/Data/Resource.resx index 4b1c11e7..9ad2e671 100644 --- a/src/OpenRiaServices.Server/Framework/Data/Resource.resx +++ b/src/OpenRiaServices.Server/Framework/Data/Resource.resx @@ -214,7 +214,7 @@ This DomainService has already been initialized. - ChangeSet has not been initialized. + The ChangeSet of this DomainService is only initialized during a 'Submit' operation, so it cannot be accessed during an operation of type '{0}'. The domain operation entry named '{0}' provides redundant functionality. Another method named '{1}' already exists. From dfc64c3868886785791722448f57242f5dcedae5 Mon Sep 17 00:00:00 2001 From: Jack Webb Date: Tue, 12 May 2026 10:35:08 +0200 Subject: [PATCH 05/10] Update changelog --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index 3f35aafc..5f921a8b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -16,6 +16,7 @@ This release gives some ❤️ to AvaloniaUI and other frameworks which use ILis * Add `IList` interface to `EntityCollection` * Nullability annotations for `EntitySet` and `EntityCollection` * Nullability annotations for many parts of the most used public API +* ChangeSet now throws an `InvalidOperationException` if accessed outside of a submit operation, eg. in the cases where `ChangeSet` previously would have returned `null`. ### Server From c65cf0198b80cfba3a2988463c8e10939865255a Mon Sep 17 00:00:00 2001 From: Jack Webb Date: Tue, 12 May 2026 10:38:57 +0200 Subject: [PATCH 06/10] Add StackTraceHiddenAttribute in PolyFills --- .../Framework/Polyfills.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/OpenRiaServices.Client/Framework/Polyfills.cs b/src/OpenRiaServices.Client/Framework/Polyfills.cs index 91b67631..9ab277e2 100644 --- a/src/OpenRiaServices.Client/Framework/Polyfills.cs +++ b/src/OpenRiaServices.Client/Framework/Polyfills.cs @@ -1,5 +1,6 @@ #if !NET using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -88,6 +89,22 @@ public CallerArgumentExpressionAttribute(string parameterName) } } +namespace System.Diagnostics +{ + /// + /// Types and Methods attributed with StackTraceHidden will be omitted from the stack trace text shown in StackTrace.ToString() + /// and Exception.StackTrace + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Struct, Inherited = false)] + public sealed class StackTraceHiddenAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public StackTraceHiddenAttribute() { } + } +} + namespace System.Diagnostics.CodeAnalysis { // From 9f16dcc6caf1eba2b3b7639cd3c8188a2bbdba6d Mon Sep 17 00:00:00 2001 From: Jack Webb Date: Tue, 12 May 2026 10:41:57 +0200 Subject: [PATCH 07/10] Update Changelog.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 5f921a8b..dc86d7d1 100644 --- a/Changelog.md +++ b/Changelog.md @@ -16,7 +16,7 @@ This release gives some ❤️ to AvaloniaUI and other frameworks which use ILis * Add `IList` interface to `EntityCollection` * Nullability annotations for `EntitySet` and `EntityCollection` * Nullability annotations for many parts of the most used public API -* ChangeSet now throws an `InvalidOperationException` if accessed outside of a submit operation, eg. in the cases where `ChangeSet` previously would have returned `null`. +* ChangeSet now throws an `InvalidOperationException` if accessed outside a submit operation, where `ChangeSet` previously returned `null`. ### Server From d04b61000236f0cf0efa0e0a99bf5f7a9da738cc Mon Sep 17 00:00:00 2001 From: Jack Webb Date: Tue, 12 May 2026 10:43:34 +0200 Subject: [PATCH 08/10] Update src/OpenRiaServices.Server/Framework/Data/DomainService.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Framework/Data/DomainService.cs | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs index 5af87003..468c17f2 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs @@ -224,15 +224,25 @@ protected ChangeSet ChangeSet { get { - if (this._changeSet == null) - ThrowChangeSetNotInitialized(this.ServiceContext.OperationType); - - return this._changeSet; - - [DoesNotReturn] - [System.Diagnostics.StackTraceHidden] - static void ThrowChangeSetNotInitialized(DomainOperationType serviceOperationType) => - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.DomainService_ChangeSetNotInitialized, serviceOperationType)); +protected ChangeSet ChangeSet +{ + get + { + if (this._changeSet == null) + ThrowChangeSetNotInitialized(this._serviceContext?.OperationType); + + return this._changeSet; + + [DoesNotReturn] + [System.Diagnostics.StackTraceHidden] + static void ThrowChangeSetNotInitialized(DomainOperationType? serviceOperationType) => + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resource.DomainService_ChangeSetNotInitialized, + serviceOperationType?.ToString() ?? "Unknown")); + } +} } } From 5e1787af1f8de1204a95a9209e682624d19c92c6 Mon Sep 17 00:00:00 2001 From: Jack Webb Date: Tue, 12 May 2026 12:41:55 +0200 Subject: [PATCH 09/10] Implement Copilot suggestions --- .../Framework/Data/DomainService.cs | 33 +++++++---------- .../Framework/Data/Resource.Designer.cs | 2 +- .../Framework/Data/Resource.resx | 2 +- .../Test/DomainServiceTests.cs | 37 ++++++++++--------- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs index 468c17f2..7753507e 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs @@ -220,32 +220,27 @@ public void Dispose() /// Gets the current . Throws if no change operations are being performed. /// /// Thrown if no change operations are being performed. + protected ChangeSet ChangeSet { get { -protected ChangeSet ChangeSet -{ - get - { - if (this._changeSet == null) - ThrowChangeSetNotInitialized(this._serviceContext?.OperationType); - - return this._changeSet; - - [DoesNotReturn] - [System.Diagnostics.StackTraceHidden] - static void ThrowChangeSetNotInitialized(DomainOperationType? serviceOperationType) => - throw new InvalidOperationException( - string.Format( - CultureInfo.CurrentCulture, - Resource.DomainService_ChangeSetNotInitialized, - serviceOperationType?.ToString() ?? "Unknown")); - } -} + if (this._changeSet == null) + ThrowChangeSetNotInitialized(); + + return this._changeSet; + + [DoesNotReturn] + [System.Diagnostics.StackTraceHidden] + static void ThrowChangeSetNotInitialized() => + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Resource.DomainService_ChangeSetNotInitialized)); } } + #endregion // Properties /// diff --git a/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs b/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs index c77e812f..66e1d1e6 100644 --- a/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs +++ b/src/OpenRiaServices.Server/Framework/Data/Resource.Designer.cs @@ -232,7 +232,7 @@ internal static string DomainService_AlreadyInitialized { } /// - /// Looks up a localized string similar to The ChangeSet of this DomainService is only initialized during a 'Submit' operation, so it cannot be accessed during an operation of type '{0}'.. + /// Looks up a localized string similar to The ChangeSet of this DomainService is only available while processing a 'Submit' operation.. /// internal static string DomainService_ChangeSetNotInitialized { get { diff --git a/src/OpenRiaServices.Server/Framework/Data/Resource.resx b/src/OpenRiaServices.Server/Framework/Data/Resource.resx index 9ad2e671..dc961bc5 100644 --- a/src/OpenRiaServices.Server/Framework/Data/Resource.resx +++ b/src/OpenRiaServices.Server/Framework/Data/Resource.resx @@ -214,7 +214,7 @@ This DomainService has already been initialized. - The ChangeSet of this DomainService is only initialized during a 'Submit' operation, so it cannot be accessed during an operation of type '{0}'. + The ChangeSet of this DomainService is only available while processing a 'Submit' operation. The domain operation entry named '{0}' provides redundant functionality. Another method named '{1}' already exists. diff --git a/src/OpenRiaServices.Server/Test/DomainServiceTests.cs b/src/OpenRiaServices.Server/Test/DomainServiceTests.cs index 5c23dd92..958d3ec7 100644 --- a/src/OpenRiaServices.Server/Test/DomainServiceTests.cs +++ b/src/OpenRiaServices.Server/Test/DomainServiceTests.cs @@ -7,18 +7,18 @@ using System.Globalization; using System.Linq; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Cities; +using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenRiaServices.Client.Test; using OpenRiaServices.EntityFramework; using OpenRiaServices.Hosting.Wcf; -using System.Xml.Linq; -using Cities; using OpenRiaServices.LinqToSql; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using TestDomainServices; -using System.Threading.Tasks; -using System.Threading; -using OpenRiaServices.Server.UnitTesting; using OpenRiaServices.Server.EntityFrameworkCore; +using OpenRiaServices.Server.UnitTesting; +using TestDomainServices; namespace OpenRiaServices.Server.Test { @@ -52,10 +52,10 @@ public void TestInitialize() /// Verify that accessing ChangeSet before SubmitAsync throws InvalidOperationException. /// [TestMethod] - public void ChangeSet_ThrowsBeforeInitialization() + public void ChangeSet_ThrowsBeforeSubmitAsync() { var ds = new ChangeSetAccessorDomainService(); - DomainServiceContext dsc = new DomainServiceContext(new MockDataService(), new MockUser("mathew"), DomainOperationType.Submit); + DomainServiceContext dsc = new DomainServiceContext(new MockDataService(), new MockUser("mathew"), DomainOperationType.Invoke); ds.Initialize(dsc); ExceptionHelper.ExpectInvalidOperationException( @@ -222,7 +222,7 @@ public void Query_InvalidIQueryable() DomainOperationEntry queryOperation = desc.GetQueryMethod("GetCities"); QueryDescription query = new QueryDescription(queryOperation, Array.Empty(), /* includeTotalCount */ true, Array.Empty().AsQueryable().Where(z => z.Code == 98052)); - ExceptionHelper.ExpectArgumentException(() => + ExceptionHelper.ExpectArgumentException(() => ds.QueryAsync(query, CancellationToken.None).GetAwaiter().GetResult() , "Expression of type 'System.Linq.EnumerableQuery`1[Cities.City]' cannot be used for parameter of type 'System.Linq.IQueryable`1[Cities.Zip]' of method 'System.Linq.IQueryable`1[Cities.Zip] Where[Zip](System.Linq.IQueryable`1[Cities.Zip], System.Linq.Expressions.Expression`1[System.Func`2[Cities.Zip,System.Boolean]])'"); } @@ -404,7 +404,7 @@ public async Task OnErrorHandling_Query() ds.Initialize(dsc); query = new QueryDescription(queryOperation, new object[] { 50 }); var queryResult = await ds.QueryAsync(query, CancellationToken.None); - + Assert.IsTrue(queryResult.HasValidationErrors, "Should have validation errors"); Assert.AreEqual(1, queryResult.ValidationErrors.Count()); Assert.IsNull(ds.LastError); @@ -618,7 +618,7 @@ public void ObjectContextExtensions_AttachAsModified() // Ensure metadata (TypeDescriptors) are initialised DomainServiceDescription.GetDescription(typeof(TestDomainServices.EF.Northwind)); - var expectedChangedProperties = new [] + var expectedChangedProperties = new[] { "CategoryName", "Description", @@ -645,7 +645,7 @@ public void ObjectContextExtensions_AttachAsModified() var currentEntry = ctx.ObjectStateManager.GetObjectStateEntry(current); string[] actualChangedProperties = currentEntry.GetModifiedProperties().ToArray(); - + CollectionAssert.AreEquivalent(expectedChangedProperties, actualChangedProperties); } @@ -715,7 +715,7 @@ public void DbContextExtensions_RoundtripAttribute_AttachAsModified() DbContextExtensions.AttachAsModified(catalog.DbContext.Products, current, original, catalog.DbContext); var currentEntry = catalog.DbContext.Entry(current); - + string[] actualNotChangedProperties = currentEntry.CurrentValues.PropertyNames.Where(p => !currentEntry.Property(p).IsModified).ToArray(); CollectionAssert.AreEquivalent(expectedNotChangedProperties, actualNotChangedProperties); @@ -2181,7 +2181,8 @@ public class TestDomainService_OverloadTests : DomainService public TestDomainService_OverloadTests() { // initialize with a test user - Initialize(new DomainServiceContext(new MockDataService(), new MockUser("mathew") { + Initialize(new DomainServiceContext(new MockDataService(), new MockUser("mathew") + { IsAuthenticated = true }, DomainOperationType.Submit)); } @@ -2515,7 +2516,8 @@ public void InsertEntity(DomainServiceInsertCustom_Entity entity) { } public void UpdateEntity(DomainServiceInsertCustom_Entity entity) { } [EntityAction] public void UpdateEntityWithInt(DomainServiceInsertCustom_Entity entity, - [CustomValidation(typeof(AlwaysFailValidator), "Validate")] int i) { } + [CustomValidation(typeof(AlwaysFailValidator), "Validate")] int i) + { } [EntityAction] public void UpdateEntityWithObject(DomainServiceInsertCustom_Entity entity, DomainServiceInsertCustom_Validated_Object obj) { } [EntityAction] @@ -2549,7 +2551,8 @@ public void InsertCity(City city) protected override ValueTask PersistChangeSetAsync(CancellationToken cancellationToken) { // Verify ChangeSet is accessible during the submit pipeline - ChangeSetWasAccessibleDuringSubmit = ChangeSet != null; + _ = ChangeSet; + ChangeSetWasAccessibleDuringSubmit = true; return new ValueTask(true); } } From c937a67aad7c57b9a3d420e6bcca8f7859f3aade Mon Sep 17 00:00:00 2001 From: Jack Webb Date: Tue, 12 May 2026 12:54:20 +0200 Subject: [PATCH 10/10] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Changelog.md | 2 +- src/OpenRiaServices.Server/Framework/Data/DomainService.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Changelog.md b/Changelog.md index dc86d7d1..cae3d347 100644 --- a/Changelog.md +++ b/Changelog.md @@ -16,10 +16,10 @@ This release gives some ❤️ to AvaloniaUI and other frameworks which use ILis * Add `IList` interface to `EntityCollection` * Nullability annotations for `EntitySet` and `EntityCollection` * Nullability annotations for many parts of the most used public API -* ChangeSet now throws an `InvalidOperationException` if accessed outside a submit operation, where `ChangeSet` previously returned `null`. ### Server +* ChangeSet now throws an `InvalidOperationException` if accessed outside a submit operation, where `ChangeSet` previously returned `null`. * Added Nullability annotations for a few core types of public API ### Other diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs index 7753507e..b0afafbb 100644 --- a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs +++ b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs @@ -234,9 +234,7 @@ protected ChangeSet ChangeSet [System.Diagnostics.StackTraceHidden] static void ThrowChangeSetNotInitialized() => throw new InvalidOperationException( - string.Format( - CultureInfo.CurrentCulture, - Resource.DomainService_ChangeSetNotInitialized)); + Resource.DomainService_ChangeSetNotInitialized); } }