diff --git a/Source/Csla.TestHelpers/TestDIContextFactory.cs b/Source/Csla.TestHelpers/TestDIContextFactory.cs index 6a7f9b6084..7300b9af68 100644 --- a/Source/Csla.TestHelpers/TestDIContextFactory.cs +++ b/Source/Csla.TestHelpers/TestDIContextFactory.cs @@ -25,7 +25,7 @@ public static class TestDIContextFactory /// Create a test DI context for testing with a default authenticated user /// /// A TestDIContext that can be used to perform testing dependent upon DI - public static TestDIContext CreateDefaultContext() + public static TestDIContext CreateDefaultContext(Action configureServices = null) { ClaimsPrincipal principal; @@ -33,7 +33,7 @@ public static TestDIContext CreateDefaultContext() principal = CreateDefaultClaimsPrincipal(); // Delegate to the other overload to create the context - return CreateContext(principal); + return CreateContext(principal, configureServices); } /// @@ -41,9 +41,9 @@ public static TestDIContext CreateDefaultContext() /// /// The principal which is to be set as the security context for Csla operations /// A TestDIContext that can be used to perform testing dependent upon DI - public static TestDIContext CreateContext(ClaimsPrincipal principal) + public static TestDIContext CreateContext(ClaimsPrincipal principal, Action configureServices = null) { - return CreateContext(null, principal); + return CreateContext(null, principal, configureServices); } /// @@ -65,7 +65,7 @@ public static TestDIContext CreateContext(Action customCslaOptions) /// The options action that is used by the consumer to configure Csla /// The principal which is to be set as the security context for Csla operations /// A TestDIContext that can be used to perform testing dependent upon DI - public static TestDIContext CreateContext(Action customCslaOptions, ClaimsPrincipal principal) + public static TestDIContext CreateContext(Action customCslaOptions, ClaimsPrincipal principal, Action configureServices = null) { IServiceProvider serviceProvider; ApplicationContext context; @@ -77,6 +77,7 @@ public static TestDIContext CreateContext(Action customCslaOptions, services.TryAddSingleton(); services.AddSingleton(); services.AddCsla(customCslaOptions); + configureServices?.Invoke(services); serviceProvider = services.BuildServiceProvider(); diff --git a/Source/Csla/Configuration/Fluent/DataPortalConfigurationExtensions.cs b/Source/Csla/Configuration/Fluent/DataPortalConfigurationExtensions.cs index 8a97e0277f..ded8577f32 100644 --- a/Source/Csla/Configuration/Fluent/DataPortalConfigurationExtensions.cs +++ b/Source/Csla/Configuration/Fluent/DataPortalConfigurationExtensions.cs @@ -8,6 +8,7 @@ using Csla.DataPortalClient; using Csla.Server; +using Csla.Server.Interceptors.ServerSide; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -100,6 +101,7 @@ internal static void AddRequiredDataPortalServices(this CslaOptions config, ISer services.TryAddTransient(typeof(IChildDataPortal<>), typeof(DataPortal<>)); services.TryAddTransient(); services.TryAddTransient(); + services.AddOptions(); services.TryAddScoped(typeof(IAuthorizeDataPortal), config.DataPortalOptions.DataPortalServerOptions.AuthorizerProviderType); foreach (Type interceptorType in config.DataPortalOptions.DataPortalServerOptions.InterceptorProviders) diff --git a/Source/Csla/Csla.csproj b/Source/Csla/Csla.csproj index 6a08769fda..b93a0b0d12 100644 --- a/Source/Csla/Csla.csproj +++ b/Source/Csla/Csla.csproj @@ -61,6 +61,7 @@ + @@ -85,6 +86,7 @@ + @@ -94,6 +96,7 @@ + @@ -103,6 +106,7 @@ + all diff --git a/Source/Csla/Server/Interceptors/ServerSide/RevalidatingInterceptor.cs b/Source/Csla/Server/Interceptors/ServerSide/RevalidatingInterceptor.cs index f63614090b..60edace5ea 100644 --- a/Source/Csla/Server/Interceptors/ServerSide/RevalidatingInterceptor.cs +++ b/Source/Csla/Server/Interceptors/ServerSide/RevalidatingInterceptor.cs @@ -9,6 +9,7 @@ using System.Collections; using Csla.Core; using Csla.Properties; +using Microsoft.Extensions.Options; namespace Csla.Server.Interceptors.ServerSide { @@ -18,15 +19,18 @@ namespace Csla.Server.Interceptors.ServerSide public class RevalidatingInterceptor : IInterceptDataPortal { private readonly ApplicationContext _applicationContext; + private readonly RevalidatingInterceptorOptions _options; /// /// Public constructor, intended to be executed by DI /// /// The context under which the DataPortal operation is executing - /// is . - public RevalidatingInterceptor(ApplicationContext applicationContext) + /// The options. + /// or is . + public RevalidatingInterceptor(ApplicationContext applicationContext, IOptions options) { _applicationContext = applicationContext ?? throw new ArgumentNullException(nameof(applicationContext)); + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); } /// @@ -46,11 +50,11 @@ public async Task InitializeAsync(InterceptArgs e) if (e is null) throw new ArgumentNullException(nameof(e)); - if (e.Parameter is not ITrackStatus checkableObject) + if (e.Parameter is not ITrackStatus checkableObject || (_options.IgnoreDeleteOperation && e.Operation == DataPortalOperations.Delete)) { return; } - + await RevalidateObjectAsync(checkableObject); if (!checkableObject.IsValid) { @@ -95,7 +99,7 @@ private async Task RevalidateObjectAsync(object? parameter) if (parameter is IUseFieldManager fieldHolder) { var properties = fieldHolder.FieldManager.GetRegisteredProperties(); - foreach (var property in properties.Where(r=>r.IsChild && fieldHolder.FieldManager.FieldExists(r))) + foreach (var property in properties.Where(r => r.IsChild && fieldHolder.FieldManager.FieldExists(r))) { var fieldData = fieldHolder.FieldManager.GetFieldData(property); if (fieldData is null) diff --git a/Source/Csla/Server/Interceptors/ServerSide/RevalidatingInterceptorOptions.cs b/Source/Csla/Server/Interceptors/ServerSide/RevalidatingInterceptorOptions.cs new file mode 100644 index 0000000000..cbee74b514 --- /dev/null +++ b/Source/Csla/Server/Interceptors/ServerSide/RevalidatingInterceptorOptions.cs @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Marimer LLC. All rights reserved. +// Website: https://cslanet.com +// +// Initiates revalidation on business objects during data portal operations +//----------------------------------------------------------------------- + +namespace Csla.Server.Interceptors.ServerSide +{ + /// + /// Options for . + /// + public class RevalidatingInterceptorOptions + { + /// + /// Indicates whether the should not re-validate business objects during a operation. + /// + public bool IgnoreDeleteOperation { get; set; } + } +} diff --git a/Source/csla.netcore.test/Server/Interceptors/ServerSide/RevalidatingInterceptorTests.cs b/Source/csla.netcore.test/Server/Interceptors/ServerSide/RevalidatingInterceptorTests.cs index 2b70e0fa8e..00237b3dd6 100644 --- a/Source/csla.netcore.test/Server/Interceptors/ServerSide/RevalidatingInterceptorTests.cs +++ b/Source/csla.netcore.test/Server/Interceptors/ServerSide/RevalidatingInterceptorTests.cs @@ -2,6 +2,8 @@ using Csla.Server; using Csla.Server.Interceptors.ServerSide; using Csla.TestHelpers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Csla.Test.Server.Interceptors.ServerSide @@ -10,6 +12,8 @@ namespace Csla.Test.Server.Interceptors.ServerSide public class RevalidatingInterceptorTests { private static TestDIContext _testDIContext; + private ApplicationContext _applicationContext; + private RevalidatingInterceptor _systemUnderTest; [ClassInitialize] public static void ClassInitialize(TestContext context) @@ -17,19 +21,31 @@ public static void ClassInitialize(TestContext context) _testDIContext = TestDIContextFactory.CreateDefaultContext(); } + [TestInitialize] + public void TestSetup() + { + _applicationContext = _testDIContext.CreateTestApplicationContext(); + _systemUnderTest = new RevalidatingInterceptor(_applicationContext, _testDIContext.ServiceProvider.GetRequiredService>()); + + PrepareApplicationContext(_applicationContext); + } + + private static void PrepareApplicationContext(ApplicationContext applicationContext) + { + applicationContext.SetExecutionLocation(ApplicationContext.ExecutionLocations.Server); + applicationContext.LocalContext["__logicalExecutionLocation"] = ApplicationContext.LogicalExecutionLocations.Server; + } + + [TestMethod] public async Task Initialize_PrimitiveCriteria_NoExceptionRaised() { // Arrange var criteria = new PrimitiveCriteria(1); - ApplicationContext applicationContext = _testDIContext.CreateTestApplicationContext(); - var sut = new RevalidatingInterceptor(applicationContext); var args = CreateUpdateArgsOfRoot(criteria); - applicationContext.SetExecutionLocation(ApplicationContext.ExecutionLocations.Server); - applicationContext.LocalContext["__logicalExecutionLocation"] = ApplicationContext.LogicalExecutionLocations.Server; // Act - await sut.InitializeAsync(args); + await _systemUnderTest.InitializeAsync(args); } [TestMethod] @@ -38,14 +54,10 @@ public async Task Initialize_ValidRootObjectNoChildren_NoExceptionRaised() // Arrange IDataPortal dataPortal = _testDIContext.CreateDataPortal(); Root rootObject = dataPortal.Fetch(new Root.Criteria("Test Data")); - ApplicationContext applicationContext = _testDIContext.CreateTestApplicationContext(); - RevalidatingInterceptor sut = new RevalidatingInterceptor(applicationContext); var args = CreateUpdateArgsOfRoot(rootObject); - applicationContext.SetExecutionLocation(ApplicationContext.ExecutionLocations.Server); - applicationContext.LocalContext["__logicalExecutionLocation"] = ApplicationContext.LogicalExecutionLocations.Server; // Act - await sut.InitializeAsync(args); + await _systemUnderTest.InitializeAsync(args); } [TestMethod] @@ -56,14 +68,10 @@ public async Task Initialize_ValidRootObjectWithChild_NoExceptionRaised() Root rootObject = dataPortal.Fetch(new Root.Criteria("Test Data")); Child childObject = rootObject.Children.AddNew(); childObject.Data = "Test child data"; - ApplicationContext applicationContext = _testDIContext.CreateTestApplicationContext(); - var sut = new RevalidatingInterceptor(applicationContext); var args = CreateUpdateArgsOfRoot(rootObject); - applicationContext.SetExecutionLocation(ApplicationContext.ExecutionLocations.Server); - applicationContext.LocalContext["__logicalExecutionLocation"] = ApplicationContext.LogicalExecutionLocations.Server; // Act - await sut.InitializeAsync(args); + await _systemUnderTest.InitializeAsync(args); } [TestMethod] @@ -76,14 +84,10 @@ public async Task Initialize_ValidRootObjectWithChildAndGrandChild_NoExceptionRa childObject.Data = "Test child data"; GrandChild grandChildObject = childObject.GrandChildren.AddNew(); grandChildObject.Data = "Test grandchild data"; - ApplicationContext applicationContext = _testDIContext.CreateTestApplicationContext(); - var sut = new RevalidatingInterceptor(applicationContext); var args = CreateUpdateArgsOfRoot(rootObject); - applicationContext.SetExecutionLocation(ApplicationContext.ExecutionLocations.Server); - applicationContext.LocalContext["__logicalExecutionLocation"] = ApplicationContext.LogicalExecutionLocations.Server; // Act - await sut.InitializeAsync(args); + await _systemUnderTest.InitializeAsync(args); } [TestMethod] @@ -92,14 +96,10 @@ public async Task Initialize_InvalidRootObject_ExceptionRaised() // Arrange IDataPortal dataPortal = _testDIContext.CreateDataPortal(); Root rootObject = dataPortal.Create(new Root.Criteria("")); - ApplicationContext applicationContext = _testDIContext.CreateTestApplicationContext(); - var sut = new RevalidatingInterceptor(applicationContext); var args = CreateUpdateArgsOfRoot(rootObject); - applicationContext.SetExecutionLocation(ApplicationContext.ExecutionLocations.Server); - applicationContext.LocalContext["__logicalExecutionLocation"] = ApplicationContext.LogicalExecutionLocations.Server; // Act and Assert - await Assert.ThrowsExceptionAsync(async () => await sut.InitializeAsync(args)); + await Assert.ThrowsExceptionAsync(async () => await _systemUnderTest.InitializeAsync(args)); } [TestMethod] @@ -109,14 +109,10 @@ public async Task Initialize_InvalidChildObject_ExceptionRaised() IDataPortal dataPortal = _testDIContext.CreateDataPortal(); Root rootObject = dataPortal.Create(new Root.Criteria("Test Data")); rootObject.Children.AddNew(); - ApplicationContext applicationContext = _testDIContext.CreateTestApplicationContext(); - var sut = new RevalidatingInterceptor(applicationContext); var args = CreateUpdateArgsOfRoot(rootObject); - applicationContext.SetExecutionLocation(ApplicationContext.ExecutionLocations.Server); - applicationContext.LocalContext["__logicalExecutionLocation"] = ApplicationContext.LogicalExecutionLocations.Server; // Act and Assert - await Assert.ThrowsExceptionAsync(async () => await sut.InitializeAsync(args)); + await Assert.ThrowsExceptionAsync(async () => await _systemUnderTest.InitializeAsync(args)); } [TestMethod] @@ -128,14 +124,29 @@ public async Task Initialize_InvalidGrandChildObject_ExceptionRaised() Child childObject = rootObject.Children.AddNew(); childObject.Data = "Test child data"; childObject.GrandChildren.AddNew(); - ApplicationContext applicationContext = _testDIContext.CreateTestApplicationContext(); - var sut = new RevalidatingInterceptor(applicationContext); var args = CreateUpdateArgsOfRoot(rootObject); - applicationContext.SetExecutionLocation(ApplicationContext.ExecutionLocations.Server); - applicationContext.LocalContext["__logicalExecutionLocation"] = ApplicationContext.LogicalExecutionLocations.Server; // Act and Assert - await Assert.ThrowsExceptionAsync(async () => await sut.InitializeAsync(args)); + await Assert.ThrowsExceptionAsync(async () => await _systemUnderTest.InitializeAsync(args)); + } + + [TestMethod] + public async Task Initialize_DeletingAnInvalidObjectDoesNotThrowWhenRevalidationForDeleteIsDisabled() + { + IDataPortal dataPortal = _testDIContext.CreateDataPortal(); + Root rootObject = dataPortal.Create(new Root.Criteria("")); + var args = new InterceptArgs(rootObject.GetType(), rootObject, DataPortalOperations.Delete, true); + + var diContext = TestDIContextFactory.CreateDefaultContext(services => + { + services.Configure(opts => opts.IgnoreDeleteOperation = true); + }); + var appContext = diContext.CreateTestApplicationContext(); + PrepareApplicationContext(appContext); + + var sut = new RevalidatingInterceptor(appContext, diContext.ServiceProvider.GetRequiredService>()); + + await sut.InitializeAsync(args); } private static InterceptArgs CreateUpdateArgsOfRoot(object parameter) => new InterceptArgs(typeof(Root), parameter, DataPortalOperations.Update, true); diff --git a/docs/Upgrading to CSLA 10.md b/docs/Upgrading to CSLA 10.md index 3fe3542aa5..a99db8c690 100644 --- a/docs/Upgrading to CSLA 10.md +++ b/docs/Upgrading to CSLA 10.md @@ -10,6 +10,18 @@ If you are upgrading from a version of CSLA prior to 8, you should review the [U TBD +## RevalidatingInterceptor + +The constructor has changed and now expects an `IOptions` instance. With this new options object it is now possible to skip the revalidation of business rules during a `Delete` operation. +To configure the new options we are using the .Net [Options pattern](https://learn.microsoft.com/en-us/dotnet/core/extensions/options). +```csharp +services.Configure(opts => +{ + opts.IgnoreDeleteOperation = true; +}); +``` + + ## Nullable Reference Types CSLA 10 supports the use of nullable reference types in your code. This means that you can use the `#nullable enable` directive in your code and the compiler will now tell you where CSLA does not expect any `null` values or returns `null`.