Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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"></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
80 changes: 78 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,85 @@ 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>();

var cp = diContext.CreateDataPortal<DelayedAsynRuleExceptionRoot>();
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>();

var cp = diContext.CreateDataPortal<DelayedAsynRuleExceptionRoot>();
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();
}

[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<DelayedAsynRuleExceptionRoot>();
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(servics => servics.AddSingleton<IUnhandledAsyncRuleExceptionHandler, TestUnhandledAsyncRuleExceptionHandler>());
private static async Task ForceThreadSwitch(DelayedAsynRuleExceptionRoot 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="DelayedAsynRuleExceptionRoot.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 DelayedAsynRuleExceptionRoot : BusinessBase<DelayedAsynRuleExceptionRoot>
{
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