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(), /* 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]])'"); } @@ -350,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); @@ -564,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", @@ -591,7 +645,7 @@ public void ObjectContextExtensions_AttachAsModified() var currentEntry = ctx.ObjectStateManager.GetObjectStateEntry(current); string[] actualChangedProperties = currentEntry.GetModifiedProperties().ToArray(); - + CollectionAssert.AreEquivalent(expectedChangedProperties, actualChangedProperties); } @@ -661,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); @@ -2127,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)); } @@ -2461,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] @@ -2479,5 +2535,26 @@ 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 + _ = ChangeSet; + ChangeSetWasAccessibleDuringSubmit = true; + return new ValueTask(true); + } + } #endregion // Mock DomainService Types } 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 +