Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions Source/Csla.TestHelpers/TestDIContextFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,25 @@ public static class TestDIContextFactory
/// Create a test DI context for testing with a default authenticated user
/// </summary>
/// <returns>A TestDIContext that can be used to perform testing dependent upon DI</returns>
public static TestDIContext CreateDefaultContext()
public static TestDIContext CreateDefaultContext(Action<IServiceCollection> configureServices = null)
{
ClaimsPrincipal principal;

// Create a default security principal
principal = CreateDefaultClaimsPrincipal();

// Delegate to the other overload to create the context
return CreateContext(principal);
return CreateContext(principal, configureServices);
}

/// <summary>
/// Create a test DI context for testing with a specific authenticated user
/// </summary>
/// <param name="principal">The principal which is to be set as the security context for Csla operations</param>
/// <returns>A TestDIContext that can be used to perform testing dependent upon DI</returns>
public static TestDIContext CreateContext(ClaimsPrincipal principal)
public static TestDIContext CreateContext(ClaimsPrincipal principal, Action<IServiceCollection> configureServices = null)
{
return CreateContext(null, principal);
return CreateContext(null, principal, configureServices);
}

/// <summary>
Expand All @@ -65,7 +65,7 @@ public static TestDIContext CreateContext(Action<CslaOptions> customCslaOptions)
/// <param name="customCslaOptions">The options action that is used by the consumer to configure Csla</param>
/// <param name="principal">The principal which is to be set as the security context for Csla operations</param>
/// <returns>A TestDIContext that can be used to perform testing dependent upon DI</returns>
public static TestDIContext CreateContext(Action<CslaOptions> customCslaOptions, ClaimsPrincipal principal)
public static TestDIContext CreateContext(Action<CslaOptions> customCslaOptions, ClaimsPrincipal principal, Action<IServiceCollection> configureServices = null)
{
IServiceProvider serviceProvider;
ApplicationContext context;
Expand All @@ -77,6 +77,7 @@ public static TestDIContext CreateContext(Action<CslaOptions> customCslaOptions,
services.TryAddSingleton<Server.Dashboard.IDashboard, Server.Dashboard.Dashboard>();
services.AddSingleton<Core.IContextManager, ApplicationContextManagerUnitTests>();
services.AddCsla(customCslaOptions);
configureServices?.Invoke(services);

serviceProvider = services.BuildServiceProvider();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

using Csla.DataPortalClient;
using Csla.Server;
using Csla.Server.Interceptors.ServerSide;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

Expand Down Expand Up @@ -100,6 +101,7 @@ internal static void AddRequiredDataPortalServices(this CslaOptions config, ISer
services.TryAddTransient(typeof(IChildDataPortal<>), typeof(DataPortal<>));
services.TryAddTransient<IDataPortalFactory, DataPortalFactory>();
services.TryAddTransient<IChildDataPortalFactory, ChildDataPortalFactory>();
services.AddOptions<RevalidatingInterceptorOptions>();

services.TryAddScoped(typeof(IAuthorizeDataPortal), config.DataPortalOptions.DataPortalServerOptions.AuthorizerProviderType);
foreach (Type interceptorType in config.DataPortalOptions.DataPortalServerOptions.InterceptorProviders)
Expand Down
4 changes: 4 additions & 0 deletions Source/Csla/Csla.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
<PackageReference Include="System.Data.Common" Version="4.3.0" />
<PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
<PackageReference Include="System.Security.Principal" Version="4.3.0" />
Expand All @@ -85,6 +86,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
Expand All @@ -94,6 +96,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
Expand All @@ -103,6 +106,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
<PackageReference Include="Polyfill" Version="7.4.0">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Collections;
using Csla.Core;
using Csla.Properties;
using Microsoft.Extensions.Options;

namespace Csla.Server.Interceptors.ServerSide
{
Expand All @@ -18,15 +19,18 @@ namespace Csla.Server.Interceptors.ServerSide
public class RevalidatingInterceptor : IInterceptDataPortal
{
private readonly ApplicationContext _applicationContext;
private readonly RevalidatingInterceptorOptions _options;

/// <summary>
/// Public constructor, intended to be executed by DI
/// </summary>
/// <param name="applicationContext">The context under which the DataPortal operation is executing</param>
/// <exception cref="ArgumentNullException"><paramref name="applicationContext"/> is <see langword="null"/>.</exception>
public RevalidatingInterceptor(ApplicationContext applicationContext)
/// <param name="options">The <see cref="RevalidatingInterceptor"/> options.</param>
/// <exception cref="ArgumentNullException"><paramref name="applicationContext"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public RevalidatingInterceptor(ApplicationContext applicationContext, IOptions<RevalidatingInterceptorOptions> options)
{
_applicationContext = applicationContext ?? throw new ArgumentNullException(nameof(applicationContext));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
}

/// <summary>
Expand All @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//-----------------------------------------------------------------------
// <copyright file="RevalidatingInterceptorOptions.cs" company="Marimer LLC">
// Copyright (c) Marimer LLC. All rights reserved.
// Website: https://cslanet.com
// </copyright>
// <summary>Initiates revalidation on business objects during data portal operations</summary>
//-----------------------------------------------------------------------

namespace Csla.Server.Interceptors.ServerSide
{
/// <summary>
/// Options for <see cref="RevalidatingInterceptor"/>.
/// </summary>
public class RevalidatingInterceptorOptions
{
/// <summary>
/// Indicates whether the <see cref="RevalidatingInterceptor"/> should not re-validate business objects during a <see cref="DataPortalOperations.Delete"/> operation.
/// </summary>
public bool IgnoreDeleteOperation { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -10,26 +12,40 @@ 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)
{
_testDIContext = TestDIContextFactory.CreateDefaultContext();
}

[TestInitialize]
public void TestSetup()
{
_applicationContext = _testDIContext.CreateTestApplicationContext();
_systemUnderTest = new RevalidatingInterceptor(_applicationContext, _testDIContext.ServiceProvider.GetRequiredService<IOptions<RevalidatingInterceptorOptions>>());

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]
Expand All @@ -38,14 +54,10 @@ public async Task Initialize_ValidRootObjectNoChildren_NoExceptionRaised()
// Arrange
IDataPortal<Root> dataPortal = _testDIContext.CreateDataPortal<Root>();
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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -92,14 +96,10 @@ public async Task Initialize_InvalidRootObject_ExceptionRaised()
// Arrange
IDataPortal<Root> dataPortal = _testDIContext.CreateDataPortal<Root>();
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<Rules.ValidationException>(async () => await sut.InitializeAsync(args));
await Assert.ThrowsExceptionAsync<Rules.ValidationException>(async () => await _systemUnderTest.InitializeAsync(args));
}

[TestMethod]
Expand All @@ -109,14 +109,10 @@ public async Task Initialize_InvalidChildObject_ExceptionRaised()
IDataPortal<Root> dataPortal = _testDIContext.CreateDataPortal<Root>();
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<Rules.ValidationException>(async () => await sut.InitializeAsync(args));
await Assert.ThrowsExceptionAsync<Rules.ValidationException>(async () => await _systemUnderTest.InitializeAsync(args));
}

[TestMethod]
Expand All @@ -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<Rules.ValidationException>(async () => await sut.InitializeAsync(args));
await Assert.ThrowsExceptionAsync<Rules.ValidationException>(async () => await _systemUnderTest.InitializeAsync(args));
}

[TestMethod]
public async Task Initialize_DeletingAnInvalidObjectDoesNotThrowWhenRevalidationForDeleteIsDisabled()
{
IDataPortal<Root> dataPortal = _testDIContext.CreateDataPortal<Root>();
Root rootObject = dataPortal.Create(new Root.Criteria(""));
var args = new InterceptArgs(rootObject.GetType(), rootObject, DataPortalOperations.Delete, true);

var diContext = TestDIContextFactory.CreateDefaultContext(services =>
{
services.Configure<RevalidatingInterceptorOptions>(opts => opts.IgnoreDeleteOperation = true);
});
var appContext = diContext.CreateTestApplicationContext();
PrepareApplicationContext(appContext);

var sut = new RevalidatingInterceptor(appContext, diContext.ServiceProvider.GetRequiredService<IOptions<RevalidatingInterceptorOptions>>());

await sut.InitializeAsync(args);
}

private static InterceptArgs CreateUpdateArgsOfRoot(object parameter) => new InterceptArgs(typeof(Root), parameter, DataPortalOperations.Update, true);
Expand Down
12 changes: 12 additions & 0 deletions docs/Upgrading to CSLA 10.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<RevalidatingInterceptorOptions>` 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<RevalidatingInterceptorOptions>(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`.
Expand Down
Loading