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
3 changes: 3 additions & 0 deletions Source/Csla/Configuration/ConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// <summary>Implement extension methods for base .NET configuration</summary>
//-----------------------------------------------------------------------
using Csla.DataPortalClient;
using Csla.Rules;
using Csla.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
Expand Down Expand Up @@ -58,6 +59,8 @@ public static IServiceCollection AddCsla(this IServiceCollection services, Actio
services.AddScoped(typeof(IDataPortalCache), cslaOptions.DataPortalOptions.DataPortalClientOptions.DataPortalCacheType);
cslaOptions.AddRequiredDataPortalServices(services);

services.AddScoped(typeof(IUnhandledAsyncRuleExceptionHandler), cslaOptions.UnhandledAsyncRuleExceptionHandlerType);

// Default to using LocalProxy and local data portal
var proxyInit = services.Any(i => i.ServiceType == typeof(IDataPortalProxy));
if (!proxyInit)
Expand Down
17 changes: 17 additions & 0 deletions Source/Csla/Configuration/Fluent/CslaOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

using System.Diagnostics.CodeAnalysis;
using Csla.Core;
using Csla.Rules;
using Microsoft.Extensions.DependencyInjection;

namespace Csla.Configuration
Expand Down Expand Up @@ -50,6 +51,22 @@ public CslaOptions UseContextManager<T>() where T : IContextManager
/// </summary>
public Type? ContextManagerType { get; private set; }

/// <summary>
/// Sets the type for the <see cref="IUnhandledAsyncRuleExceptionHandler"/> to be used.
/// </summary>
/// <typeparam name="T">The type to register for <see cref="IUnhandledAsyncRuleExceptionHandler"/>.</typeparam>
/// <returns>This instance.</returns>
public CslaOptions UseUnhandledAsyncRuleExceptionHandler<T>() where T : IUnhandledAsyncRuleExceptionHandler
{
UnhandledAsyncRuleExceptionHandlerType = typeof(T);
return this;
}

/// <summary>
/// Gets the type used for the <see cref="IUnhandledAsyncRuleExceptionHandler"/>.
/// </summary>
public Type UnhandledAsyncRuleExceptionHandlerType { get; private set; } = typeof(DontObserveUnhandledAsyncRuleExceptionHandler);

/// <summary>
/// Sets a value indicating whether CSLA
/// should fallback to using reflection instead of
Expand Down
2 changes: 1 addition & 1 deletion Source/Csla/Core/BusinessBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,7 @@ protected BusinessRules BusinessRules
get
{
if (_businessRules == null)
_businessRules = new BusinessRules(ApplicationContext, this);
_businessRules = new BusinessRules(ApplicationContext, this, ApplicationContext.GetRequiredService<IUnhandledAsyncRuleExceptionHandler>());
else if (_businessRules.Target == null)
_businessRules.SetTarget(this);
return _businessRules;
Expand Down
14 changes: 11 additions & 3 deletions Source/Csla/Rules/BusinessRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ public BusinessRules()
/// </summary>
/// <param name="applicationContext"></param>
/// <param name="target">Target business object.</param>
/// <param name="unhandledAsyncRuleExceptionHandler">Handler for unhandled exceptions that occur during asynchronous rule execution.</param>
/// <exception cref="ArgumentNullException"><paramref name="applicationContext"/> or <paramref name="target"/> is <see langword="null"/>.</exception>
public BusinessRules(ApplicationContext applicationContext, IHostRules target)
public BusinessRules(ApplicationContext applicationContext, IHostRules target, IUnhandledAsyncRuleExceptionHandler unhandledAsyncRuleExceptionHandler)
{
_applicationContext = applicationContext ?? throw new ArgumentNullException(nameof(applicationContext));
_target = target ?? throw new ArgumentNullException(nameof(target));
_unhandledAsyncRuleExceptionHandler = unhandledAsyncRuleExceptionHandler ?? throw new ArgumentNullException(nameof(unhandledAsyncRuleExceptionHandler));
}

#if NET9_0_OR_GREATER
Expand All @@ -49,6 +51,8 @@ public BusinessRules(ApplicationContext applicationContext, IHostRules target)
[NonSerialized]
private object _syncRoot = new();
#endif
[NonSerialized]
private readonly IUnhandledAsyncRuleExceptionHandler _unhandledAsyncRuleExceptionHandler;

private ApplicationContext _applicationContext;

Expand Down Expand Up @@ -1120,7 +1124,7 @@ private RunRulesResult RunRules(IEnumerable<IBusinessRuleBase> rules, bool casca
if (rule is IBusinessRule syncRule)
syncRule.Execute(context);
else if (rule is IBusinessRuleAsync asyncRule)
RunAsyncRule(asyncRule, context);
RunAsyncRule(asyncRule, context, _unhandledAsyncRuleExceptionHandler);
else
throw new ArgumentOutOfRangeException(rule.GetType().FullName);
}
Expand Down Expand Up @@ -1151,12 +1155,16 @@ private RunRulesResult RunRules(IEnumerable<IBusinessRuleBase> rules, bool casca
return new RunRulesResult(affectedProperties, dirtyProperties);
}

private static async void RunAsyncRule(IBusinessRuleAsync asyncRule, IRuleContext context)
private static async void RunAsyncRule(IBusinessRuleAsync asyncRule, IRuleContext context, IUnhandledAsyncRuleExceptionHandler unhandledAsyncRuleExceptionHandler)
{
try
{
await asyncRule.ExecuteAsync(context);
}
catch (Exception exc) when (unhandledAsyncRuleExceptionHandler.CanHandle(exc, asyncRule))
{
await unhandledAsyncRuleExceptionHandler.Handle(exc, asyncRule, context);
}
finally
{
context.Complete();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Csla.Rules;

internal class DontObserveUnhandledAsyncRuleExceptionHandler : IUnhandledAsyncRuleExceptionHandler
{
public bool CanHandle(Exception exception, IBusinessRuleBase executingRule) => false;
public ValueTask Handle(Exception exception, IBusinessRuleBase executingRule, IRuleContext ruleContext) => throw new NotSupportedException();
}
24 changes: 24 additions & 0 deletions Source/Csla/Rules/IUnhandledAsyncRuleExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Csla.Rules;

/// <summary>
/// Represents an interface for handling exceptions raised by <see cref="IBusinessRuleAsync"/>.
/// </summary>
public interface IUnhandledAsyncRuleExceptionHandler
{
/// <summary>
/// Checks whether the given <paramref name="exception"/> and <paramref name="executingRule"/> can be handled.
/// </summary>
/// <param name="exception">The unhandled <see cref="Exception"/>.</param>
/// <param name="executingRule">The rule causing <paramref name="exception"/>.</param>
/// <returns><see langword="true"/> if the exception can be handled. Otherwise <see langword="false"/>.</returns>
bool CanHandle(Exception exception, IBusinessRuleBase executingRule);

/// <summary>
/// Handles the raised <paramref name="exception"/>.
/// </summary>
/// <param name="exception">The unhandled <see cref="Exception"/>.</param>
/// <param name="executingRule">The rule causing <paramref name="exception"/>.</param>
/// <param name="ruleContext">The associated <see cref="IRuleContext"/> to this rule run.</param>
/// <returns><see cref="ValueTask"/></returns>
ValueTask Handle(Exception exception, IBusinessRuleBase executingRule, IRuleContext ruleContext);
}
2 changes: 2 additions & 0 deletions Source/tests/Csla.test/Csla.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Csla.Generators\cs\AutoImplementProperties\Csla.Generator.AutoImplementProperties.Attributes.CSharp\Csla.Generator.AutoImplementProperties.Attributes.CSharp.csproj" ReferenceOutputAssembly="true" OutputItemType="Analyzer" PrivateAssets="all" />
<ProjectReference Include="..\..\Csla.Generators\cs\AutoImplementProperties\Csla.Generator.AutoImplementProperties.CSharp\Csla.Generator.AutoImplementProperties.CSharp.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
<ProjectReference Include="..\Csla.TestHelpers\Csla.TestHelpers.csproj" />
<ProjectReference Include="..\..\Csla\Csla.csproj" />
</ItemGroup>
Expand Down
83 changes: 81 additions & 2 deletions Source/tests/Csla.test/ValidationRules/AsyncRuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
// <summary>This only works on Silverlight because when run through NUnit it is not running</summary>
//-----------------------------------------------------------------------

using Csla.Rules;
using Csla.TestHelpers;
using FluentAssertions.Execution;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Csla.Test.ValidationRules
Expand Down Expand Up @@ -152,11 +154,88 @@ public async Task MyTestMethod()
await har.WaitForIdle();

var affectedProperties = await har.CheckRulesForPropertyAsyncAwait();
using (new AssertionScope())
using (new AssertionScope())
{
har.AsyncAwait.Should().Be("abc");
affectedProperties.Should().ContainSingle().Which.Should().Be(nameof(AsyncRuleRoot.AsyncAwait));
}
}

[TestMethod($"When an async rule throws an exception the framework must invoke {nameof(IUnhandledAsyncRuleExceptionHandler)}.{nameof(IUnhandledAsyncRuleExceptionHandler.CanHandle)}.")]
public async Task AsyncRuleException_Testcase01()
{
var diContext = CreateDIContextForAsyncRuleExceptions();

var unhandledExceptionHandler = (TestUnhandledAsyncRuleExceptionHandler)diContext.ServiceProvider.GetRequiredService<IUnhandledAsyncRuleExceptionHandler>();
unhandledExceptionHandler.CanHandleResult = true; // Otherwise the test host process will be crashed

var cp = diContext.CreateDataPortal<DelayedAsyncRuleExceptionRoot>();
var bo = await cp.CreateAsync();

var tcs = new TaskCompletionSource();
unhandledExceptionHandler.CanHandleInspector = (_, _) => tcs.SetResult();

await ForceThreadSwitch(bo, TimeSpan.FromMilliseconds(25));

await tcs.Task.WaitAsync(TimeSpan.FromMilliseconds(150));
}

[TestMethod($"When an async rules exception can be handled by {nameof(IUnhandledAsyncRuleExceptionHandler)} it must invoke {nameof(IUnhandledAsyncRuleExceptionHandler)}.{nameof(IUnhandledAsyncRuleExceptionHandler.Handle)}.")]
public async Task AsyncRuleException_Testcase02()
{
var diContext = CreateDIContextForAsyncRuleExceptions();

var unhandledExceptionHandler = (TestUnhandledAsyncRuleExceptionHandler)diContext.ServiceProvider.GetRequiredService<IUnhandledAsyncRuleExceptionHandler>();
unhandledExceptionHandler.CanHandleResult = true; // Otherwise the test host process will be crashed

var cp = diContext.CreateDataPortal<DelayedAsyncRuleExceptionRoot>();
var bo = await cp.CreateAsync();

bool canHandleInvoked = false;
unhandledExceptionHandler.HandleInspector = (_, _, _) => canHandleInvoked = true;

await ForceThreadSwitch(bo, TimeSpan.FromMilliseconds(25));

await Task.Delay(TimeSpan.FromMilliseconds(150));

canHandleInvoked.Should().BeTrue();
}

[Ignore("This test can be run but will crash the test host, so this is failing any CI build. But for completness it's here and can be run if necessary.")]
[TestMethod($"When the default {nameof(IUnhandledAsyncRuleExceptionHandler)} is used the exception must be handled by a global unhandled exception handler (for this test it's the {nameof(AppDomain)}.{nameof(AppDomain.CurrentDomain)}.{nameof(AppDomain.CurrentDomain.UnhandledException)} event).")]
public async Task AsyncRuleException_Testcase03()
{
var diContext = CreateDIContextForAsyncRuleExceptions();

bool unobservedTaskExceptionFound = false;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
try
{
var unhandledExceptionHandler = (TestUnhandledAsyncRuleExceptionHandler)diContext.ServiceProvider.GetRequiredService<IUnhandledAsyncRuleExceptionHandler>();
unhandledExceptionHandler.CanHandleResult = false;
var cp = diContext.CreateDataPortal<DelayedAsyncRuleExceptionRoot>();
var bo = await cp.CreateAsync();

await ForceThreadSwitch(bo, TimeSpan.FromMilliseconds(25));

await Task.Delay(TimeSpan.FromMilliseconds(150));

unobservedTaskExceptionFound.Should().BeTrue();
}
finally
{
AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException;
}

void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) => unobservedTaskExceptionFound = true;
}


private static TestDIContext CreateDIContextForAsyncRuleExceptions() => TestDIContextFactory.CreateDefaultContext(services => services.AddSingleton<IUnhandledAsyncRuleExceptionHandler, TestUnhandledAsyncRuleExceptionHandler>());
private static async Task ForceThreadSwitch(DelayedAsyncRuleExceptionRoot root, TimeSpan exceptionDelay)
{
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
root.ExceptionDelay = exceptionDelay;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//-----------------------------------------------------------------------
// <copyright file="DelayedAsyncRuleExceptionRoot.cs" company="Marimer LLC">
// Copyright (c) Marimer LLC. All rights reserved.
// Website: https://cslanet.com
// </copyright>
// <summary>This only works on Silverlight because when run through NUnit it is not running</summary>
//-----------------------------------------------------------------------

using Csla.Core;
using Csla.Rules;

namespace Csla.Test.ValidationRules
{
[CslaImplementProperties]
internal partial class DelayedAsyncRuleExceptionRoot : BusinessBase<DelayedAsyncRuleExceptionRoot>
{
public partial TimeSpan ExceptionDelay { get; set; }

[Create, RunLocal]
private void Create()
{
_ = this;
}

protected override void AddBusinessRules()
{
base.AddBusinessRules();
BusinessRules.AddRule(new RaiseExceptionRuleAsync(ExceptionDelayProperty));
}


private class RaiseExceptionRuleAsync : BusinessRuleAsync
{
public RaiseExceptionRuleAsync(IPropertyInfo primaryProperty) : base(primaryProperty)
{
InputProperties.Add(primaryProperty);
}

protected override async Task ExecuteAsync(IRuleContext context)
{
var delay = context.GetInputValue<TimeSpan>(PrimaryProperty);

await Task.Delay(delay);

throw new InvalidOperationException("This is a test exception");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//-----------------------------------------------------------------------
// <copyright file="TestUnhandledAsyncRuleExceptionHandler.cs" company="Marimer LLC">
// Copyright (c) Marimer LLC. All rights reserved.
// Website: https://cslanet.com
// </copyright>
// <summary>This only works on Silverlight because when run through NUnit it is not running</summary>
//-----------------------------------------------------------------------

using Csla.Rules;

namespace Csla.Test.ValidationRules
{
internal class TestUnhandledAsyncRuleExceptionHandler : IUnhandledAsyncRuleExceptionHandler
{
public bool CanHandleResult { get; set; } = true;

public Action<Exception, IBusinessRuleBase> CanHandleInspector { get; set; }

public bool CanHandle(Exception exception, IBusinessRuleBase executingRule)
{
CanHandleInspector?.Invoke(exception, executingRule);
return CanHandleResult;
}


public Action<Exception, IBusinessRuleBase, IRuleContext> HandleInspector { get; set; }
public ValueTask Handle(Exception exception, IBusinessRuleBase executingRule, IRuleContext ruleContext)
{
HandleInspector?.Invoke(exception, executingRule, ruleContext);
return ValueTask.CompletedTask;
}
}
}
22 changes: 20 additions & 2 deletions docs/Upgrading to CSLA 10.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ services.Configure<RevalidatingInterceptorOptions>(opts =>
});
```

## Exception from asynchronous rules
A new API is added to make it possible to handle exceptions thrown by asynchronous rules.
The new interface to implement is `Csla.Rules.IUnhandledAsyncRuleExceptionHandler` which has two methods
* `bool CanHandle(Exception, IBusinessRuleBase)`
* to decide whether this exception should be handled or not

* `ValueTask Handle(Exception, IBusinessRuleBase, IRuleContext)`
* to handle the exception when `CanHandle(...) == true`
With these methods you can now decide whether to handle the exception and how or let the exception be unobserved bubble up and potentially cause a crash.

You can register your implementation in two ways
* Just add the implementation to your service collection `services.AddScoped<IUnhandledAsyncRuleExceptionHandler, YourImplementation>()`
* Use `services.AddCsla(o => o.UseUnhandledAsyncRuleExceptionHandler<YourImplementation>());`. The handler is registered as scoped.

The _default_ is still no handling of any exception thrown in an asynchronous rule.


## Nullable Reference Types

Expand Down Expand Up @@ -62,5 +78,7 @@ Supporting nullable types means that some APIs have changed to support nullable


## Breaking changes
* `Csla.Server.DataPortal` constructor changes:
* Removed unused parameters: `IDataPortalActivator activator`, `IDataPortalExceptionInspector exceptionInspector`
* `Csla.Server.DataPortal` constructor changed.
* Removed unused parameters: `IDataPortalActivator activator`, `IDataPortalExceptionInspector exceptionInspector`.
* `Csla.Rules.BusinessRules` constructor changed.
* New parameter `IUnhandledAsyncRuleExceptionHandler` added to support the new asynchronous rule exception handling.
Loading