diff --git a/packages/extensions.dependency-injection.abstractions/src/extensions/ServiceCollectionDescriptorExtensions.ts b/packages/extensions.dependency-injection.abstractions/src/extensions/ServiceCollectionDescriptorExtensions.ts index 8b2e934d..65d3a372 100644 --- a/packages/extensions.dependency-injection.abstractions/src/extensions/ServiceCollectionDescriptorExtensions.ts +++ b/packages/extensions.dependency-injection.abstractions/src/extensions/ServiceCollectionDescriptorExtensions.ts @@ -20,7 +20,21 @@ export function tryAddServiceDescriptor( export function tryAddServiceDescriptorIterable( collection: IServiceCollection, descriptor: ServiceDescriptor, +): void; +export function tryAddServiceDescriptorIterable( + collection: IServiceCollection, + descriptors: ServiceDescriptor[], +): void; +export function tryAddServiceDescriptorIterable( + collection: IServiceCollection, + descriptorOrDescriptors: ServiceDescriptor | ServiceDescriptor[], ): void { - // TODO - tryAddServiceDescriptor(collection, descriptor); + if (descriptorOrDescriptors instanceof Array) { + for (const descriptor of descriptorOrDescriptors) { + tryAddServiceDescriptor(collection, descriptor); + } + } else { + // TODO + tryAddServiceDescriptor(collection, descriptorOrDescriptors); + } } diff --git a/packages/third-party.mediatr/src/mediatr/IPipelineBehavior.ts b/packages/third-party.mediatr/src/mediatr/IPipelineBehavior.ts new file mode 100644 index 00000000..c908e803 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/IPipelineBehavior.ts @@ -0,0 +1,9 @@ +export type RequestHandlerDelegate = () => Promise; + +// https://github.com/jbogard/MediatR/blob/e611b1bcc00b10244810abeea2f7c62f155beb5e/src/MediatR/IPipelineBehavior.cs#L20 +export interface IPipelineBehavior { + handle( + request: TRequest, + next: RequestHandlerDelegate, + ): Promise; +} diff --git a/packages/third-party.mediatr/src/mediatr/Mediatr.ts b/packages/third-party.mediatr/src/mediatr/Mediatr.ts index 671e7aed..6810409c 100644 --- a/packages/third-party.mediatr/src/mediatr/Mediatr.ts +++ b/packages/third-party.mediatr/src/mediatr/Mediatr.ts @@ -10,9 +10,17 @@ import { NotificationHandlerWrapper, NotificationHandlerWrapperImpl, } from './wrappers/NotificationHandlerWrapper'; +import { + RequestHandlerBase, + RequestHandlerWrapperImpl, +} from './wrappers/RequestHandlerWrapper'; // https://github.com/jbogard/MediatR/blob/f4de8196adafd37faff274ce819ada93a3d7531b/src/MediatR/Mediator.cs#L16 export class Mediator implements IMediator { + private readonly requestHandlers = new Map< + Ctor>, + RequestHandlerBase + >(); private readonly notificationHandlers = new Map< Ctor, NotificationHandlerWrapper @@ -26,7 +34,16 @@ export class Mediator implements IMediator { ) {} send(request: IRequest): Promise { - throw new Error('Method not implemented.'); + const handler = getOrAdd( + this.requestHandlers, + request.constructor as Ctor>, + (requestCtor) => { + const wrapper = new RequestHandlerWrapperImpl(requestCtor); + return wrapper as RequestHandlerBase; + }, + ); + + return handler.handle(request, this.serviceProvider); } /** diff --git a/packages/third-party.mediatr/src/mediatr/extensions-di/MediatRServiceConfig.ts b/packages/third-party.mediatr/src/mediatr/extensions-di/MediatRServiceConfig.ts index 7a29a1e6..c917a00c 100644 --- a/packages/third-party.mediatr/src/mediatr/extensions-di/MediatRServiceConfig.ts +++ b/packages/third-party.mediatr/src/mediatr/extensions-di/MediatRServiceConfig.ts @@ -1,11 +1,14 @@ import { Ctor } from '@yohira/base'; -import { ServiceLifetime } from '@yohira/extensions.dependency-injection.abstractions'; +import { + ServiceDescriptor, + ServiceLifetime, +} from '@yohira/extensions.dependency-injection.abstractions'; import { INotificationPublisher } from '../INotificationPublisher'; import { Mediator } from '../Mediatr'; import { ForOfAwaitPublisher } from '../notification-publishers/ForOfAwaitPublisher'; -// https://github.com/jbogard/MediatR/blob/f4de8196adafd37faff274ce819ada93a3d7531b/src/MediatR/MicrosoftExtensionsDI/MediatrServiceConfiguration.cs#L9 +// https://github.com/jbogard/MediatR/blob/8b1ee39fe29fd12c8042d22f9fd1a969b5fcbc16/src/MediatR/MicrosoftExtensionsDI/MediatrServiceConfiguration.cs#L12 export class MediatRServiceConfig { /** * Mediator implementation type to register. Default is {@link Mediator} @@ -23,4 +26,12 @@ export class MediatRServiceConfig { * Service lifetime to register services under. Default value is {@link ServiceLifetime.Transient} */ lifetime: ServiceLifetime = ServiceLifetime.Transient; + /** + * List of request pre processors to register in specific order + */ + requestPreProcessorsToRegister: ServiceDescriptor[] = []; + /** + * List of request post processors to register in specific order + */ + requestPostProcessorsToRegister: ServiceDescriptor[] = []; } diff --git a/packages/third-party.mediatr/src/mediatr/pipeline/RequestPostProcessorBehavior.ts b/packages/third-party.mediatr/src/mediatr/pipeline/RequestPostProcessorBehavior.ts new file mode 100644 index 00000000..a31c37d2 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/pipeline/RequestPostProcessorBehavior.ts @@ -0,0 +1,19 @@ +import { + IPipelineBehavior, + RequestHandlerDelegate, +} from '../IPipelineBehavior'; + +// https://github.com/jbogard/MediatR/blob/e611b1bcc00b10244810abeea2f7c62f155beb5e/src/MediatR/Pipeline/RequestPostProcessorBehavior.cs#L12 +/** + * Behavior for executing all instances after handling the request + */ +export class RequestPostProcessorBehavior + implements IPipelineBehavior +{ + handle( + request: TRequest, + next: RequestHandlerDelegate, + ): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/third-party.mediatr/src/mediatr/pipeline/RequestPreProcessorBehavior.ts b/packages/third-party.mediatr/src/mediatr/pipeline/RequestPreProcessorBehavior.ts new file mode 100644 index 00000000..9885021a --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/pipeline/RequestPreProcessorBehavior.ts @@ -0,0 +1,19 @@ +import { + IPipelineBehavior, + RequestHandlerDelegate, +} from '../IPipelineBehavior'; + +// https://github.com/jbogard/MediatR/blob/e611b1bcc00b10244810abeea2f7c62f155beb5e/src/MediatR/Pipeline/RequestPreProcessorBehavior.cs#L12 +/** + * Behavior for executing all instances before handling a request + */ +export class RequestPreProcessorBehavior + implements IPipelineBehavior +{ + handle( + request: TRequest, + next: RequestHandlerDelegate, + ): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/third-party.mediatr/src/mediatr/registrar/ServiceRegistrar.ts b/packages/third-party.mediatr/src/mediatr/registrar/ServiceRegistrar.ts index d0224eb4..efdf2fa1 100644 --- a/packages/third-party.mediatr/src/mediatr/registrar/ServiceRegistrar.ts +++ b/packages/third-party.mediatr/src/mediatr/registrar/ServiceRegistrar.ts @@ -3,11 +3,14 @@ import { ServiceDescriptor, ServiceLifetime, tryAddServiceDescriptor, + tryAddServiceDescriptorIterable, } from '@yohira/extensions.dependency-injection.abstractions'; import { IMediator } from '../IMediator'; import { INotificationPublisher } from '../INotificationPublisher'; import { MediatRServiceConfig } from '../extensions-di/MediatRServiceConfig'; +import { RequestPostProcessorBehavior } from '../pipeline/RequestPostProcessorBehavior'; +import { RequestPreProcessorBehavior } from '../pipeline/RequestPreProcessorBehavior'; // https://github.com/jbogard/MediatR/blob/43fb46f39020ab4880fefe75fa2315351f347742/src/MediatR/Registration/ServiceRegistrar.cs#L300 export function addRequiredServices( @@ -38,4 +41,38 @@ export function addRequiredServices( ); tryAddServiceDescriptor(services, notificationPublisherServiceDescriptor); + + // TODO + + if (serviceConfig.requestPreProcessorsToRegister.length > 0) { + tryAddServiceDescriptorIterable( + services, + ServiceDescriptor.fromCtor( + ServiceLifetime.Transient, + Symbol.for('IPipelineBehavior<>'), + RequestPreProcessorBehavior, + ), + ); + tryAddServiceDescriptorIterable( + services, + serviceConfig.requestPreProcessorsToRegister, + ); + } + + if (serviceConfig.requestPostProcessorsToRegister.length > 0) { + tryAddServiceDescriptorIterable( + services, + ServiceDescriptor.fromCtor( + ServiceLifetime.Transient, + Symbol.for('IPipelineBehavior<>'), + RequestPostProcessorBehavior, + ), + ); + tryAddServiceDescriptorIterable( + services, + serviceConfig.requestPostProcessorsToRegister, + ); + } + + // TODO } diff --git a/packages/third-party.mediatr/src/mediatr/wrappers/RequestHandlerWrapper.ts b/packages/third-party.mediatr/src/mediatr/wrappers/RequestHandlerWrapper.ts new file mode 100644 index 00000000..e754b852 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/wrappers/RequestHandlerWrapper.ts @@ -0,0 +1,62 @@ +import { Ctor, IServiceProvider } from '@yohira/base'; +import { + getRequiredService, + getServices, +} from '@yohira/extensions.dependency-injection.abstractions'; + +import { IRequest } from '../../mediatr.contracts/IRequest'; +import { + IPipelineBehavior, + RequestHandlerDelegate, +} from '../IPipelineBehavior'; +import { IRequestHandler } from '../IRequestHandler'; + +// https://github.com/jbogard/MediatR/blob/761fb0b1b420f5a8c2cb4a751617dce7ab9c3fe3/src/MediatR/Wrappers/RequestHandlerWrapper.cs#L9 +export abstract class RequestHandlerBase { + abstract handle( + request: object, + serviceProvider: IServiceProvider, + ): Promise; +} + +// https://github.com/jbogard/MediatR/blob/761fb0b1b420f5a8c2cb4a751617dce7ab9c3fe3/src/MediatR/Wrappers/RequestHandlerWrapper.cs#L15 +export abstract class RequestHandlerWrapper< + TResponse, +> extends RequestHandlerBase { + abstract handle( + request: IRequest, + serviceProvider: IServiceProvider, + ): Promise; +} + +// https://github.com/jbogard/MediatR/blob/761fb0b1b420f5a8c2cb4a751617dce7ab9c3fe3/src/MediatR/Wrappers/RequestHandlerWrapper.cs#L27 +export class RequestHandlerWrapperImpl< + TRequest extends IRequest, + TResponse, +> extends RequestHandlerWrapper { + constructor(private readonly requestCtor: Ctor) { + super(); + } + + handle( + request: IRequest, + serviceProvider: IServiceProvider, + ): Promise { + const handler = (): Promise => + getRequiredService>( + serviceProvider, + Symbol.for(`IRequestHandler<${this.requestCtor.name}>`), + ).handle(request as TRequest); + + return getServices>( + serviceProvider, + Symbol.for(`IPipelineBehavior<${this.requestCtor.name}>`), + ) + .reverse() + .reduce( + (next, pipeline) => () => + pipeline.handle(request as TRequest, next), + handler as RequestHandlerDelegate, + )(); + } +} diff --git a/packages/third-party.mediatr/test/Send.test.ts b/packages/third-party.mediatr/test/Send.test.ts new file mode 100644 index 00000000..933072b9 --- /dev/null +++ b/packages/third-party.mediatr/test/Send.test.ts @@ -0,0 +1,219 @@ +import { IServiceProvider } from '@yohira/base'; +import { buildServiceProvider } from '@yohira/extensions.dependency-injection'; +import { + ServiceCollection, + addSingletonInstance, + addTransientCtor, + getRequiredService, + inject, +} from '@yohira/extensions.dependency-injection.abstractions'; +import { + IMediator, + IRequest, + IRequestHandler, + addMediatR, +} from '@yohira/third-party.mediatr'; +import { beforeEach, expect, test } from 'vitest'; + +class Pong { + message: string | undefined; +} + +class Ping implements IRequest { + message: string | undefined; +} + +class VoidPing implements IRequest {} + +class PingHandler implements IRequestHandler { + handle(request: Ping): Promise { + return Promise.resolve( + ((): Pong => { + const pong = new Pong(); + pong.message = request.message + ' Pong'; + return pong; + })(), + ); + } +} + +class Dependency { + called = false; +} + +class VoidPingHandler implements IRequestHandler { + constructor( + @inject(Symbol.for('Dependency')) + private readonly dependency: Dependency, + ) {} + + handle(): Promise { + this.dependency.called = true; + + return Promise.resolve(); + } +} + +class GenericPing implements IRequest { + pong: T | undefined; +} + +class GenericPingHandler + implements IRequestHandler, T> +{ + constructor( + @inject(Symbol.for('Dependency')) + private readonly dependency: Dependency, + ) {} + + handle(request: GenericPing): Promise { + this.dependency.called = true; + request.pong!.message += ' Pong'; + return Promise.resolve(request.pong!); + } +} + +class VoidGenericPing implements IRequest {} + +class VoidGenericPingHandler + implements IRequestHandler, void> +{ + constructor( + @inject(Symbol.for('Dependency')) + private readonly dependency: Dependency, + ) {} + + handle(): Promise { + this.dependency.called = true; + + return Promise.resolve(); + } +} + +let serviceProvider: IServiceProvider; +let dependency: Dependency; +let mediator: IMediator; + +beforeEach(() => { + dependency = new Dependency(); + const services = new ServiceCollection(); + addTransientCtor( + services, + Symbol.for('IRequestHandler'), + PingHandler, + ); + addTransientCtor( + services, + Symbol.for('IRequestHandler'), + VoidPingHandler, + ); + addTransientCtor( + services, + Symbol.for('IRequestHandler'), + GenericPingHandler, + ); + addTransientCtor( + services, + Symbol.for('IRequestHandler'), + VoidGenericPingHandler, + ); + addMediatR(services, (cfg) => {}); + addSingletonInstance(services, Symbol.for('Dependency'), dependency); + serviceProvider = buildServiceProvider(services); + mediator = getRequiredService(serviceProvider, IMediator); +}); + +// https://github.com/jbogard/MediatR/blob/1552d92a35081119a5fae9454b74b56ffdf046f6/test/MediatR.Tests/SendTests.cs#L109 +test('Should_resolve_main_handler', async () => { + const response = await mediator.send( + ((): Ping => { + const ping = new Ping(); + ping.message = 'Ping'; + return ping; + })(), + ); + + expect(response.message).toBe('Ping Pong'); +}); + +// https://github.com/jbogard/MediatR/blob/1552d92a35081119a5fae9454b74b56ffdf046f6/test/MediatR.Tests/SendTests.cs#L117C23-L117C55 +test('Should_resolve_main_void_handler', async () => { + await mediator.send(new VoidPing()); + + expect(dependency.called).toBe(true); +}); + +// https://github.com/jbogard/MediatR/blob/1552d92a35081119a5fae9454b74b56ffdf046f6/test/MediatR.Tests/SendTests.cs#L125C23-L125C71 +test('Should_resolve_main_handler_via_dynamic_dispatch', async () => { + const request = ((): Ping => { + const ping = new Ping(); + ping.message = 'Ping'; + return ping; + })(); + const response = await mediator.send(request); + + const pong = response; + expect(pong).toBeInstanceOf(Pong); + expect(pong.message).toBe('Ping Pong'); +}); + +// https://github.com/jbogard/MediatR/blob/1552d92a35081119a5fae9454b74b56ffdf046f6/test/MediatR.Tests/SendTests.cs#L135C23-L135C76 +test('Should_resolve_main_void_handler_via_dynamic_dispatch', async () => { + const request = new VoidPing(); + const response = await mediator.send(request); + + expect(response).toBeUndefined(); + + expect(dependency.called).toBe(true); +}); + +// https://github.com/jbogard/MediatR/blob/1552d92a35081119a5fae9454b74b56ffdf046f6/test/MediatR.Tests/SendTests.cs#L146C23-L146C72 +test('Should_resolve_main_handler_by_specific_interface', async () => { + const response = await mediator.send( + ((): Ping => { + const ping = new Ping(); + ping.message = 'Ping'; + return ping; + })(), + ); + + expect(response.message).toBe('Ping Pong'); +}); + +// https://github.com/jbogard/MediatR/blob/1552d92a35081119a5fae9454b74b56ffdf046f6/test/MediatR.Tests/SendTests.cs#L154C23-L154C69 +test('Should_resolve_main_handler_by_given_interface', async () => { + const requests = [new VoidPing()]; + await mediator.send(requests[0]); + + expect(dependency.called).toBe(true); +}); + +// TODO + +// https://github.com/jbogard/MediatR/blob/1552d92a35081119a5fae9454b74b56ffdf046f6/test/MediatR.Tests/SendTests.cs#L167C23-L167C53 +test('Should_resolve_generic_handler', async () => { + const request = ((): GenericPing => { + const genericPong = new GenericPing(); + genericPong.pong = ((): Pong => { + const pong = new Pong(); + pong.message = 'Ping'; + return pong; + })(); + return genericPong; + })(); + const result = await mediator.send(request); + + const pong = result; + expect(pong).toBeInstanceOf(Pong); + expect(pong.message).toBe('Ping Pong'); + + expect(dependency.called).toBe(true); +}); + +// https://github.com/jbogard/MediatR/blob/1552d92a35081119a5fae9454b74b56ffdf046f6/test/MediatR.Tests/SendTests.cs#L179C23-L179C58 +test('Should_resolve_generic_void_handler', async () => { + const request = new VoidGenericPing(); + await mediator.send(request); + + expect(dependency.called).toBe(true); +});