diff --git a/Changelog.md b/Changelog.md
index 3f35aafc..cae3d347 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -19,6 +19,7 @@ This release gives some ❤️ to AvaloniaUI and other frameworks which use ILis
### 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.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
{
//
diff --git a/src/OpenRiaServices.Server/Framework/Data/DomainService.cs b/src/OpenRiaServices.Server/Framework/Data/DomainService.cs
index dac892e2..b0afafbb 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,16 +217,28 @@ 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.
///
- protected ChangeSet? ChangeSet
+ /// Thrown if no change operations are being performed.
+
+ protected ChangeSet ChangeSet
{
get
{
+ if (this._changeSet == null)
+ ThrowChangeSetNotInitialized();
+
return this._changeSet;
+
+ [DoesNotReturn]
+ [System.Diagnostics.StackTraceHidden]
+ static void ThrowChangeSetNotInitialized() =>
+ throw new InvalidOperationException(
+ Resource.DomainService_ChangeSetNotInitialized);
}
}
+
#endregion // Properties
///
@@ -718,7 +731,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 +764,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 +779,7 @@ await this.InvokeCudOperationsAsync()
await this.InvokeCustomOperations()
.ConfigureAwait(false);
- return !this.ChangeSet!.HasError;
+ return !this.ChangeSet.HasError;
}
///
@@ -794,7 +807,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 +997,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 +1016,7 @@ private async Task PersistChangeSetAsyncInternal(CancellationToken cancell
}
}
- return !this.ChangeSet!.HasError;
+ return !this.ChangeSet.HasError;
}
///
@@ -1040,7 +1053,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 +1095,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..66e1d1e6 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 The ChangeSet of this DomainService is only available while processing a 'Submit' operation..
+ ///
+ 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..dc961bc5 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.
+
+ 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 c75e076e..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
{
@@ -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_ThrowsBeforeSubmitAsync()
+ {
+ var ds = new ChangeSetAccessorDomainService();
+ DomainServiceContext dsc = new DomainServiceContext(new MockDataService(), new MockUser("mathew"), DomainOperationType.Invoke);
+ 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.
@@ -168,7 +222,7 @@ public void Query_InvalidIQueryable()
DomainOperationEntry queryOperation = desc.GetQueryMethod("GetCities");
QueryDescription query = new QueryDescription(queryOperation, Array.Empty