Skip to content

[Issue 3112] Use the configured transaction provider when no override is supplied#3525

Merged
iancooper merged 9 commits into
BrighterCommand:masterfrom
dhickie:issues/3112
Feb 27, 2025
Merged

[Issue 3112] Use the configured transaction provider when no override is supplied#3525
iancooper merged 9 commits into
BrighterCommand:masterfrom
dhickie:issues/3112

Conversation

@dhickie

@dhickie dhickie commented Feb 21, 2025

Copy link
Copy Markdown
Contributor

This PR fixes issue #3112, in which an InvalidCastException was thrown when calling Post or PostAsync on the CommandProcessor if a persistent outbox was registered.

It does this by switching to a reflection based approach when calling methods on the OutboxProducerMediator, removing the need for the problematic cast. The transaction type for the generic method call is taken from the registered transaction provider during construction, and defaults to CommittableTransaction if no transaction provider is registered.

In an effort to simplify usage of the CommandProcessor, it also makes a couple of other changes:

  • If a call is made to the overload of DepositPost or DepositPostAsync that doesn't take a transaction provider, it uses the one that was registered along with the persistent outbox (if any).
  • If a call is made to the overload of DepositPost or DepositPostAsync that does take a transaction provider, but the transaction type for the supplied transaction provider doesn't match the type that was registered with the persistent outbox (if any), an InvalidOperationException is thrown.

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates Failed
Prevent hotspot decline (1 hotspot with Code Duplication)
Enforce advisory code health rules (1 file with Code Duplication)

Gates Passed
2 Quality Gates Passed

See analysis details in CodeScene

Reason for failure
Prevent hotspot decline Violations Code Health Impact
CommandProcessor.cs 1 rule in this hotspot 8.28 → 7.33 Suppress
Enforce advisory code health rules Violations Code Health Impact
CommandProcessor.cs 1 advisory rule 8.28 → 7.33 Suppress

Quality Gate Profile: Clean Code Collective
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.

@iancooper iancooper left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see if we can move away from a spy here, and use our InMemoryOutbox and possibly add an InMemoryTransaction and InMemoryTransactionProvider


namespace Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles
{
internal class SpyOutbox :

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use the InMemoryOutbox? We are trying to avoid Test Doubles where the in-memory version could substitute. If we need to query the state of the outbox, is that something that is all outboxed, or should the in-memory outbox be exposed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with using the InMemoryOutbox is that it's the default implementation, and the scenario being tested here is when the user uses something other than the default, since that was what led to this issue being found in the first place (I also wouldn't have found that we should be clearing the bound MethodInfo's when we clear the outbox mediator from the CommandProcessor).

To do that without a test-only Spy implementation we would need to either have an additional type of in memory outbox in the core package (which feels redundant?), or pull in one of the other outbox implementations (and then require all the associated infrastructure in order to be able to run the core tests). This feels like the cleanest solution to me.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, that makes sense in this context. Closed.


namespace Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles;

public class SpyTransaction

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to in-memory, does this imply that perhaps we need to swap from CommittableTransacton to our own InMemoryTransaction to use?


namespace Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles;

public class SpyTransactionProvider : IAmABoxTransactionProvider<SpyTransaction>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, does this imply an InMemoryTransactionProvider?

Comment on lines +107 to +111
private static readonly ConcurrentDictionary<string, MethodInfo> s_boundDepositCalls = new();
private static readonly ConcurrentDictionary<string, MethodInfo> s_boundDepositCallsAsync = new();
private static readonly ConcurrentDictionary<string, MethodInfo> s_boundBulkDepositCalls = new();
private static readonly ConcurrentDictionary<string, MethodInfo> s_boundBulkDepositCallsAsync = new();
private static readonly ConcurrentDictionary<string, MethodInfo> s_boundMediatorMethods = new();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are changing this, what about using Delegate rather than MethodInfo.Invoke?

private static readonly ConcurrentDictionary<string, Func<....>> s_boundDepositCallsAsync = new();

....

var del =method.CreateDelegate<Func<...>>();
s_boundDepositCallsAsync .Add(key, del);

...

var del = s_boundDepositCallsAsync[key];
del(.....);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few reasons not to use Delegates here. If you look at how the code would change, you'd be going from this:

deposit = depositMethod?.MakeGenericMethod(actualRequestType, transactionType)!;
return (deposit?.Invoke(this, [actualRequest, amABoxTransactionProvider, requestContext, dictionary, batchId]) as string)!;

To this:

deposit = depositMethod?.MakeGenericMethod(actualRequestType, transactionType)!;
var delegate = deposit.CreateDelegate(
    typeof(Func<IRequest, IAmABoxTransactionProvider, RequestContext, Dictionary<string, object>, string, Type, string>),
    this);
return (delegate.DynamicInvoke(actualRequest, amABoxTransactionProvider, requestContext, dictionary, batchId) as string)!;

By storing the methods as MethodInfo's, we can also store multiple methods with different signatures in the same collection, as we do with the the bound mediator methods in s_boundMediatorMethods. If they were delegates, each mediator method would need a separate member variable. So overall, using delegates makes the code more verbose without any real benefit in terms of clarity.

Lastly, using Delegate.DynamicInvoke is actually much slower than MethodInfo.Invoke, and given we're at the heart of things here we want it to be as performant as possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should also say that MethodInfo.CreateDelegate<T> doesn't exist in netstandard2.0, which we still support at the moment. Hence the need for Delegate.DynamicInvoke.

# Conflicts:
#	src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs
#	src/Paramore.Brighter/CommandProcessor.cs
#	src/Paramore.Brighter/CommandProcessorBuilder.cs

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates Passed
4 Quality Gates Passed

See analysis details in CodeScene

Quality Gate Profile: Clean Code Collective
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates Passed
4 Quality Gates Passed

See analysis details in CodeScene

Quality Gate Profile: Clean Code Collective
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.

@iancooper

Copy link
Copy Markdown
Member

I think we could merge this in if you want @dhickie?

@iancooper iancooper merged commit 3db024c into BrighterCommand:master Feb 27, 2025
@dhickie dhickie deleted the issues/3112 branch March 21, 2025 13:48
DevJonny pushed a commit to DevJonny/Brighter that referenced this pull request Feb 28, 2026
… is supplied (BrighterCommand#3525)

* Update DepositPost and DepositPostAsync to use default transaction provider if an override is not supplied, plus throw an exception if a transaction provider is supplied with a different transaction type from the one configured.

* Update Post and PostAsync to use the default transaction provider if an override is not supplied.

* Flow the transaction provider through from the CommandProcessorBuilder to the CommandProcessor

* Fix binding generic methods of the wrong type to invocation targets & wrapping exceptions thrown inside invocations

* Add DepositPost tests

* Add Post tests & clear bound CommandProcessor methods when clearing the mediator

* Fix broken CommandProcessor test setup
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants