New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added option to enable PublishAsync to invoke handlers sequentially #101

Closed
wants to merge 2 commits into
base: master
from

Conversation

Projects
None yet
7 participants
@samueldjack

samueldjack commented Sep 1, 2016

This helps to address #98 by adding a configuration option to enable invoking async notification handlers sequentially rather than in parallel.

@@ -73,20 +88,65 @@ public void Publish(INotification notification)
public Task PublishAsync(IAsyncNotification notification)
{

This comment has been minimized.

@abatishchev

abatishchev Sep 1, 2016

To not introduce breaking change, I'd make this an overload having the default behavior.

@abatishchev

abatishchev Sep 1, 2016

To not introduce breaking change, I'd make this an overload having the default behavior.

This comment has been minimized.

@samueldjack

samueldjack Sep 2, 2016

The problem with this is that I'd then have to update every call site in my code to get the behaviour I want - since I want sequential behaviour whenever events are published.

If the default option on the Mediator is configured correctly then there would be no breaking change in behaviour irrespective of how the method is called.

@samueldjack

samueldjack Sep 2, 2016

The problem with this is that I'd then have to update every call site in my code to get the behaviour I want - since I want sequential behaviour whenever events are published.

If the default option on the Mediator is configured correctly then there would be no breaking change in behaviour irrespective of how the method is called.

This comment has been minimized.

@abatishchev

abatishchev Sep 2, 2016

I see. This would be an issue for me as well, will have to update every call site. So settings behavior in one place is a good idea.

@abatishchev

abatishchev Sep 2, 2016

I see. This would be an issue for me as well, will have to update every call site. So settings behavior in one place is a good idea.

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Sep 1, 2016

hey @samueldjack, thanks for converting my question to a pr!

abatishchev commented Sep 1, 2016

hey @samueldjack, thanks for converting my question to a pr!

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Sep 10, 2016

@jbogard any chance to review please?

abatishchev commented Sep 10, 2016

@jbogard any chance to review please?

@dotnetjunkie

This comment has been minimized.

Show comment
Hide comment
@dotnetjunkie

dotnetjunkie Sep 10, 2016

@abatishchev

Adding one more ctor is a breaking chance since some DI containers (such as Simple Injector) will stop working

This shouldn't be a problem, since you shouldn't use your container's auto-wiring capabilities to create types of external libraries and frameworks, because of exactly this. Instead register external types using a delegate. This way you have full control over the constructor that is called.

Note that this is not a Simple Injector specific thing. This advice holds for all containers. Every container has its own unique way to select constructors.

dotnetjunkie commented Sep 10, 2016

@abatishchev

Adding one more ctor is a breaking chance since some DI containers (such as Simple Injector) will stop working

This shouldn't be a problem, since you shouldn't use your container's auto-wiring capabilities to create types of external libraries and frameworks, because of exactly this. Instead register external types using a delegate. This way you have full control over the constructor that is called.

Note that this is not a Simple Injector specific thing. This advice holds for all containers. Every container has its own unique way to select constructors.

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Sep 11, 2016

Hi Steven! I don't say it's a problem with SI or there are no workarounds. But if one already uses their container's auto-wiring capability than it will be a breaking change for them.
I'm not quite conveinced why one should not use it if it works and works flawlessly. In particular, MediatR has single ctor from the very beginning so why don't write Register<IMediator, Mediator>() rather than Register<IMediator>(() => new Mediator(...)) when you can? When you can't then you can't, and obviously have no other options. But until that it's nice to have such a choice.
That being said, is there workaround or other (and better) way to achieve the same? Yes. Would it be a breaking change? Yes. Would it be a big deal? No.

abatishchev commented Sep 11, 2016

Hi Steven! I don't say it's a problem with SI or there are no workarounds. But if one already uses their container's auto-wiring capability than it will be a breaking change for them.
I'm not quite conveinced why one should not use it if it works and works flawlessly. In particular, MediatR has single ctor from the very beginning so why don't write Register<IMediator, Mediator>() rather than Register<IMediator>(() => new Mediator(...)) when you can? When you can't then you can't, and obviously have no other options. But until that it's nice to have such a choice.
That being said, is there workaround or other (and better) way to achieve the same? Yes. Would it be a breaking change? Yes. Would it be a big deal? No.

@dotnetjunkie

This comment has been minimized.

Show comment
Hide comment
@dotnetjunkie

dotnetjunkie Sep 11, 2016

But if one already uses their container's auto-wiring capability than it will be a breaking change for them.

Be careful calling this a breaking change, because this would limit a reusable library builder like Jimmy to make changes to its library that would usually be stated as 'non-breaking' by The Framework Design Guidelines.

Adding overloaded constructors is typically the way for a framework builder to make these changes, because the signature of a constructor can't be changed (since that already is a breaking change).

I don't say it's a problem with SI

I would even say that this is less the problem with Simple Injector. In the case of Simple Injector you get a very clear exception message, which allows you to change your configuration. With a container that does constructor resolution, the framework builder never really knows which constructor will be selected (since each DI container has other rules for selecting constructors). And when you upgrade your application to the newest version of that external library, everything might keep compiling and no error is thrown when the application runs, but the application could still function incorrectly (but without warning or error), because the wrong constructor has be selected by your container.

If Jimmy follows the FDG (and I know he does), he should be able to add constructor overloads. If this breaks your application, it's not Jimmy's fault; it's yours.

so why don't write Register<IMediator, Mediator>() rather than Register(() => new Mediator(...)) when you can?

It's important to make the distinction between types you own and control and for which you know they have just a single constructor, and types of external libraries where you don't have any control over the number of constructors they have. In that case, my advice is to always call the constructor using C# code, not through reflection. Calling such constructor with normal C# code should not cause any maintenance problems, because their constructor signature will never change, compared to the types you control. We typically see constructor signatures change regularly while developing an application.

Of course I must admit here that MediatR is not your typical external library, so depending on how we see MediatR, the above description might not completely hold for MediatR.

When we follow the SOLID principles, they guide us to hiding external libraries from our application code; we create application-specific abstractions and create adapters to hide the external libraries behind. This however does not work with MediatR, since its goal is to provide you with the core abstractions to build your application around. So in this sense, MediatR must perhaps be seen as the central part of our application, otherwise we would be breaking SOLID big time.

On the other hand, MediatR is a reusable library used in hundreds (or perhaps even thousands) of applications and meant for general use. This immediately causes conflict with it being a central part of the application, since its changes and updates are out of control of the application, and its types must be treated differently than other application types (because of the possible additions of constructors for instance).

So whether or not you use auto-wiring to build up MediatR types depends on where you place MediatR in your application architecture, but do keep in mind that you don’t control MediatR.

dotnetjunkie commented Sep 11, 2016

But if one already uses their container's auto-wiring capability than it will be a breaking change for them.

Be careful calling this a breaking change, because this would limit a reusable library builder like Jimmy to make changes to its library that would usually be stated as 'non-breaking' by The Framework Design Guidelines.

Adding overloaded constructors is typically the way for a framework builder to make these changes, because the signature of a constructor can't be changed (since that already is a breaking change).

I don't say it's a problem with SI

I would even say that this is less the problem with Simple Injector. In the case of Simple Injector you get a very clear exception message, which allows you to change your configuration. With a container that does constructor resolution, the framework builder never really knows which constructor will be selected (since each DI container has other rules for selecting constructors). And when you upgrade your application to the newest version of that external library, everything might keep compiling and no error is thrown when the application runs, but the application could still function incorrectly (but without warning or error), because the wrong constructor has be selected by your container.

If Jimmy follows the FDG (and I know he does), he should be able to add constructor overloads. If this breaks your application, it's not Jimmy's fault; it's yours.

so why don't write Register<IMediator, Mediator>() rather than Register(() => new Mediator(...)) when you can?

It's important to make the distinction between types you own and control and for which you know they have just a single constructor, and types of external libraries where you don't have any control over the number of constructors they have. In that case, my advice is to always call the constructor using C# code, not through reflection. Calling such constructor with normal C# code should not cause any maintenance problems, because their constructor signature will never change, compared to the types you control. We typically see constructor signatures change regularly while developing an application.

Of course I must admit here that MediatR is not your typical external library, so depending on how we see MediatR, the above description might not completely hold for MediatR.

When we follow the SOLID principles, they guide us to hiding external libraries from our application code; we create application-specific abstractions and create adapters to hide the external libraries behind. This however does not work with MediatR, since its goal is to provide you with the core abstractions to build your application around. So in this sense, MediatR must perhaps be seen as the central part of our application, otherwise we would be breaking SOLID big time.

On the other hand, MediatR is a reusable library used in hundreds (or perhaps even thousands) of applications and meant for general use. This immediately causes conflict with it being a central part of the application, since its changes and updates are out of control of the application, and its types must be treated differently than other application types (because of the possible additions of constructors for instance).

So whether or not you use auto-wiring to build up MediatR types depends on where you place MediatR in your application architecture, but do keep in mind that you don’t control MediatR.

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Sep 12, 2016

Indeed, these all make sense, even somewhere I could argue, but I'm a not good speaker and educator than you, so I have no other options than admit your rightness :)
The only thing I meant saying breaking change was not against such change but to bump major version. Right after

abatishchev commented Sep 12, 2016

Indeed, these all make sense, even somewhere I could argue, but I'm a not good speaker and educator than you, so I have no other options than admit your rightness :)
The only thing I meant saying breaking change was not against such change but to bump major version. Right after

Implemented changes suggested in discussion thread:
* pushed initiation of Tasks into the WhenAll handler
* set the default value of PublishAsyncOptions in the constructor for clarity
@jbogard

This comment has been minimized.

Show comment
Hide comment
@jbogard

jbogard Sep 12, 2016

Owner

So....I don't like this for the main reason that it's very specific. I intentionally try to leave behavior out of the mix in the Mediator class, because it's opaque, you can't see what it does.

What I'd rather do is allow you to customize the behavior in any way you like, but not through a switch/if-then. Even if this is just a thing where we structure things so that you can subclass and override a virtual method, I'd rather do that than such a specific configuration here.

Owner

jbogard commented Sep 12, 2016

So....I don't like this for the main reason that it's very specific. I intentionally try to leave behavior out of the mix in the Mediator class, because it's opaque, you can't see what it does.

What I'd rather do is allow you to customize the behavior in any way you like, but not through a switch/if-then. Even if this is just a thing where we structure things so that you can subclass and override a virtual method, I'd rather do that than such a specific configuration here.

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Sep 12, 2016

So maybe for a separate behavior to have a separate method? Let PublishAsync() run in parallel and something else (e.g. PipelineAsync() or PublishSequnceAsync()) run sequentially?

But still passing an options parameter where one specifies the behavior is quite visible imo.

abatishchev commented Sep 12, 2016

So maybe for a separate behavior to have a separate method? Let PublishAsync() run in parallel and something else (e.g. PipelineAsync() or PublishSequnceAsync()) run sequentially?

But still passing an options parameter where one specifies the behavior is quite visible imo.

@jbogard

This comment has been minimized.

Show comment
Hide comment
@jbogard

jbogard Sep 12, 2016

Owner

No - no options parameter. If something was going to be passed in, I'd rather it be a separate interface. INotificationPublisher or similar. No if/switch statement, make it a strategy pattern.

But overall, I'd rather subclass-and-override first. At least give people the option to add their own behaviors first before having a breaking change like this.

Owner

jbogard commented Sep 12, 2016

No - no options parameter. If something was going to be passed in, I'd rather it be a separate interface. INotificationPublisher or similar. No if/switch statement, make it a strategy pattern.

But overall, I'd rather subclass-and-override first. At least give people the option to add their own behaviors first before having a breaking change like this.

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Sep 12, 2016

Accepting INotificationPublisher makes sense. I'd ship MediatR with 2 implementations: Sequential and Parallel.

Or indeed subclass-and-override. Inherit Mediator by SequentialMediator and override PublishAsync, or decorate Mediator with SequentialMediatorDecoratory, proxy all methods but PublishAsync. Is this something you would ship with MediatR by default? If yes, I would submit a separate pull request.

abatishchev commented Sep 12, 2016

Accepting INotificationPublisher makes sense. I'd ship MediatR with 2 implementations: Sequential and Parallel.

Or indeed subclass-and-override. Inherit Mediator by SequentialMediator and override PublishAsync, or decorate Mediator with SequentialMediatorDecoratory, proxy all methods but PublishAsync. Is this something you would ship with MediatR by default? If yes, I would submit a separate pull request.

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Sep 13, 2016

Please check this draft out: an PublishAsync() overload accepting IAsyncNotificationPublisher with its default implementation. Drawbacks: the implementation is internal since depends on internal wrapper class.

Here's another draft: a sequential decorator. Drawbacks: had to extract GetNotifications() method into few internal helpers to avoid code duplication.

abatishchev commented Sep 13, 2016

Please check this draft out: an PublishAsync() overload accepting IAsyncNotificationPublisher with its default implementation. Drawbacks: the implementation is internal since depends on internal wrapper class.

Here's another draft: a sequential decorator. Drawbacks: had to extract GetNotifications() method into few internal helpers to avoid code duplication.

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Sep 13, 2016

The task would be simplified once #67 is merged.

abatishchev commented Sep 13, 2016

The task would be simplified once #67 is merged.

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Sep 13, 2016

Also if make cache static (and internal) as per #73 the task would be simplified even more.

abatishchev commented Sep 13, 2016

Also if make cache static (and internal) as per #73 the task would be simplified even more.

@jbogard jbogard referenced this pull request Sep 15, 2016

Closed

MediatR 3.0 ideas #102

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Sep 21, 2016

I published one more draft: this time SequentialMediator inherits Mediator and overrides PublishAsync in a manner that no internal classes have to be made public.

abatishchev commented Sep 21, 2016

I published one more draft: this time SequentialMediator inherits Mediator and overrides PublishAsync in a manner that no internal classes have to be made public.

@samueldjack

This comment has been minimized.

Show comment
Hide comment
@samueldjack

samueldjack Sep 22, 2016

Having thought about this further, I'm inclined to lean away from the sub-classing approach towards injecting a behaviour into the mediator. The biggest factor for me is that the subclassing approach immediately limits composability. It only affects one aspect of the Mediators behaviour (async behaviour) and yet it layers on top of the whole class.

Imagine another scenario where the sync behaviour needed to be customised and a subclass was introduced to support that. Then imagine wanting to support both the custom sync behaviour and the custom async behaviour. The only way to do that would be to reimplement the one behaviour as a further subclass of the other.

I guess the issue here is that the Mediator is a facade, pulling a couple of different kinds of behaviour (sync and async publishing) into one convenient interface. Thus the design needs to allow for independent variations in how the behaviours behind the facade are implemented - or split the behaviours as #102 is suggesting.

samueldjack commented Sep 22, 2016

Having thought about this further, I'm inclined to lean away from the sub-classing approach towards injecting a behaviour into the mediator. The biggest factor for me is that the subclassing approach immediately limits composability. It only affects one aspect of the Mediators behaviour (async behaviour) and yet it layers on top of the whole class.

Imagine another scenario where the sync behaviour needed to be customised and a subclass was introduced to support that. Then imagine wanting to support both the custom sync behaviour and the custom async behaviour. The only way to do that would be to reimplement the one behaviour as a further subclass of the other.

I guess the issue here is that the Mediator is a facade, pulling a couple of different kinds of behaviour (sync and async publishing) into one convenient interface. Thus the design needs to allow for independent variations in how the behaviours behind the facade are implemented - or split the behaviours as #102 is suggesting.

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Sep 27, 2016

@samueldjack sorry, missed to respond in time, I completely agree!

abatishchev commented Sep 27, 2016

@samueldjack sorry, missed to respond in time, I completely agree!

@jbogard

This comment has been minimized.

Show comment
Hide comment
@jbogard

jbogard Dec 7, 2016

Owner

OK so this is all getting changed in 3.0. Stay tuned.

Owner

jbogard commented Dec 7, 2016

OK so this is all getting changed in 3.0. Stay tuned.

@jbogard jbogard closed this Dec 7, 2016

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev commented Dec 7, 2016

Cool!

@jblackburn21

This comment has been minimized.

Show comment
Hide comment
@jblackburn21

jblackburn21 Jan 5, 2017

I can work on a PR to v3 if a direction is decided. This would now be need for async and cancellable async handlers.

Do you want to go the IAsyncNotificationPublisher route, and default to the parallel implementation?

jblackburn21 commented Jan 5, 2017

I can work on a PR to v3 if a direction is decided. This would now be need for async and cancellable async handlers.

Do you want to go the IAsyncNotificationPublisher route, and default to the parallel implementation?

@jbogard

This comment has been minimized.

Show comment
Hide comment
@jbogard

jbogard Jan 5, 2017

Owner

Any direction that does not involve adding new constructor arguments. That'd be a breaking change and I'd need to go to 4.0.

Owner

jbogard commented Jan 5, 2017

Any direction that does not involve adding new constructor arguments. That'd be a breaking change and I'd need to go to 4.0.

@jbogard

This comment has been minimized.

Show comment
Hide comment
@jbogard

jbogard Jan 5, 2017

Owner

That's why I suggested just a protected virtual method. It enables you to do whatever you want.

Owner

jbogard commented Jan 5, 2017

That's why I suggested just a protected virtual method. It enables you to do whatever you want.

@codearoo

This comment has been minimized.

Show comment
Hide comment
@codearoo

codearoo Feb 2, 2017

Hi... I was just looking at similar topics. I found where I can create a IAsyncNotificationHandler handler which is nice to allow the handler to decide if it should block the flow.. but is it in the works still to also allow the publishing code to determine if it cares if subsequent handlers are async or not? The Publish() method (using ver 3.0.0) is blocking until all non-async handlers finish.

codearoo commented Feb 2, 2017

Hi... I was just looking at similar topics. I found where I can create a IAsyncNotificationHandler handler which is nice to allow the handler to decide if it should block the flow.. but is it in the works still to also allow the publishing code to determine if it cares if subsequent handlers are async or not? The Publish() method (using ver 3.0.0) is blocking until all non-async handlers finish.

@abatishchev

This comment has been minimized.

Show comment
Hide comment
@abatishchev

abatishchev Feb 2, 2017

If you're looking for a fire-and-forget behavior you can decorate your handler with one that would enqueue on the thread pool inner handler's Handle() in one or another way, e.g.:

public FireAndForgetAsyncNotificationHandlerDecorator : IAsyncNotificationHandler<TNotification>
    where TNotification : INotification
{
    private readonly IAsyncNotificationHandler<TNotification> _inner;

    public FireAndForgetAsyncNotificationHandlerDecorator (IAsyncNotificationHandler<TNotification> inner)
    {
        _inner = inner;
    }

    public async Task Handle(TNotification notification)
    {
        await Task.Run(async () => await _inner.Handle(notification));
    }
}

(I don't guarantee this code will work as described but hope you got the idea)

abatishchev commented Feb 2, 2017

If you're looking for a fire-and-forget behavior you can decorate your handler with one that would enqueue on the thread pool inner handler's Handle() in one or another way, e.g.:

public FireAndForgetAsyncNotificationHandlerDecorator : IAsyncNotificationHandler<TNotification>
    where TNotification : INotification
{
    private readonly IAsyncNotificationHandler<TNotification> _inner;

    public FireAndForgetAsyncNotificationHandlerDecorator (IAsyncNotificationHandler<TNotification> inner)
    {
        _inner = inner;
    }

    public async Task Handle(TNotification notification)
    {
        await Task.Run(async () => await _inner.Handle(notification));
    }
}

(I don't guarantee this code will work as described but hope you got the idea)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment