1
0

mediatr-dispatch-synthesizer.test.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. /**
  2. * MediatR request/notification dispatch bridge (C#/.NET).
  3. *
  4. * MediatR decouples a `_mediator.Send(x)` / `_mediator.Publish(x)` call from the `Handle`
  5. * method that runs it, linked by the request/notification TYPE (the `IRequestHandler<T,…>`
  6. * generic). This bridges each mediator dispatch → the `Handle` of the matching handler.
  7. * The sent type is resolved from the argument three ways — inline `new X(...)`, a local
  8. * `var v = new X(...)`, and a parameter/local declared `X v` — and precision rests on two
  9. * gates proven here: the receiver must be mediator-ish (a `MessagingCenter.Send` is ignored),
  10. * and the type must have a handler (an `IRequest` with no handler is never bridged). Covers
  11. * `IRequest<T>`, void `IRequest` (single-arg `IRequestHandler<T>`), and `INotification`.
  12. */
  13. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  14. import * as fs from 'node:fs';
  15. import * as path from 'node:path';
  16. import * as os from 'node:os';
  17. import { CodeGraph } from '../src';
  18. describe('mediatr-dispatch synthesizer', () => {
  19. let dir: string;
  20. beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mediatr-dispatch-')); });
  21. afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
  22. const write = (rel: string, body: string) => {
  23. const p = path.join(dir, rel);
  24. fs.mkdirSync(path.dirname(p), { recursive: true });
  25. fs.writeFileSync(p, body);
  26. };
  27. it('bridges Send/Publish to the matching Handle across inline, local, and param arg forms', async () => {
  28. write('Requests.cs', `namespace Shop;
  29. using MediatR;
  30. public record GetThingsQuery : IRequest<ThingsVm>;
  31. public record CreateThingCommand(string Name) : IRequest<int>;
  32. public record DeleteThingCommand(int Id) : IRequest;
  33. public record ThingDeletedNotification(int Id) : INotification;
  34. public class UnhandledCommand : IRequest<int> { }
  35. `);
  36. write('Handlers.cs', `namespace Shop;
  37. using MediatR;
  38. using System.Threading;
  39. using System.Threading.Tasks;
  40. public class GetThingsQueryHandler : IRequestHandler<GetThingsQuery, ThingsVm> {
  41. public Task<ThingsVm> Handle(GetThingsQuery request, CancellationToken ct) => Task.FromResult(new ThingsVm());
  42. }
  43. public class CreateThingCommandHandler : IRequestHandler<CreateThingCommand, int> {
  44. public Task<int> Handle(CreateThingCommand request, CancellationToken ct) => Task.FromResult(1);
  45. }
  46. public class DeleteThingCommandHandler : IRequestHandler<DeleteThingCommand> {
  47. public Task Handle(DeleteThingCommand request, CancellationToken ct) => Task.CompletedTask;
  48. }
  49. public class ThingDeletedNotificationHandler : INotificationHandler<ThingDeletedNotification> {
  50. public Task Handle(ThingDeletedNotification notification, CancellationToken ct) => Task.CompletedTask;
  51. }
  52. `);
  53. write('ThingsController.cs', `namespace Shop;
  54. using MediatR;
  55. using System.Threading.Tasks;
  56. public class ThingsController {
  57. private readonly ISender _mediator;
  58. public ThingsController(ISender mediator) { _mediator = mediator; }
  59. public async Task GetThings() {
  60. var vm = await _mediator.Send(new GetThingsQuery());
  61. }
  62. public async Task Create(CreateThingCommand command) {
  63. var id = await _mediator.Send(command);
  64. }
  65. public async Task Delete(int id) {
  66. var command = new DeleteThingCommand(id);
  67. await _mediator.Send(command);
  68. }
  69. public async Task Notify(int id) {
  70. await _mediator.Publish(new ThingDeletedNotification(id));
  71. }
  72. public async Task Bogus() {
  73. await _mediator.Send(new UnhandledCommand());
  74. }
  75. public void ViaMessagingCenter() {
  76. MessagingCenter.Send(this, "evt", new CreateThingCommand("x"));
  77. }
  78. }
  79. `);
  80. const cg = await CodeGraph.init(dir, { silent: true });
  81. await cg.indexAll();
  82. const db = (cg as any).db.db;
  83. const edges = db
  84. .prepare(
  85. `SELECT s.name source, t.name target, json_extract(e.metadata,'$.via') via
  86. FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
  87. WHERE json_extract(e.metadata,'$.synthesizedBy') = 'mediatr-dispatch'`
  88. )
  89. .all();
  90. // Four bridged dispatches: inline (GetThings, Notify), param-typed (Create), local var (Delete).
  91. expect(edges.map((r: any) => r.source).sort()).toEqual(['Create', 'Delete', 'GetThings', 'Notify']);
  92. expect([...new Set(edges.map((r: any) => r.via))].sort()).toEqual([
  93. 'CreateThingCommand', 'DeleteThingCommand', 'GetThingsQuery', 'ThingDeletedNotification',
  94. ]);
  95. // Every target is a Handle method.
  96. expect(edges.every((r: any) => r.target === 'Handle')).toBe(true);
  97. // PRECISION: an IRequest with no handler is never bridged; a non-mediator .Send is ignored.
  98. expect(edges.some((r: any) => r.via === 'UnhandledCommand')).toBe(false);
  99. expect(edges.some((r: any) => r.source === 'ViaMessagingCenter')).toBe(false);
  100. cg.close?.();
  101. });
  102. it('produces no edges in a C# project with no MediatR (clean control)', async () => {
  103. write('Service.cs', `namespace Shop;
  104. public class Service {
  105. private readonly IRepo _repo;
  106. public Service(IRepo repo) { _repo = repo; }
  107. public string Find(string id) => _repo.Get(id);
  108. }
  109. `);
  110. const cg = await CodeGraph.init(dir, { silent: true });
  111. await cg.indexAll();
  112. const db = (cg as any).db.db;
  113. const count = db
  114. .prepare(`SELECT count(*) c FROM edges WHERE json_extract(metadata,'$.synthesizedBy') = 'mediatr-dispatch'`)
  115. .get();
  116. expect(count.c).toBe(0);
  117. cg.close?.();
  118. });
  119. });