Skip to content

merge-graphs: a contract type both repos declare stays two unconnected nodes #3007

Description

@durmazoguzhan

merge-graphs prefixes every node id with its repo tag, so a contract type that two services both declare becomes two unconnected nodes. On a message bus that is the one hop worth having: the producer names the message type in one repo, the consumer implements IConsumer<T> on the same type in the other, and nothing in the merged graph gets you from one to the other.

Concretely, from a merged graph of two .NET services:

p_CatalogService::...syncproductupserttosearchevent        <- .UpdateProductOnSearchAppAsync() references it
p_ElasticsearchService::...syncproductupserttosearchevent   <- ProductUpsertConsumer references it

Same namespace, same name, no edge between them. path from the producing method to the consumer returns nothing, so the flow reads as two unrelated halves.

Why name plus namespace is a usable signal here

I was expecting to need something structural and measured it first. Between the two services, 1440 and 262 sourced type declarations:

  • 7 pairs agree on both namespace and name
  • all 7 are the shared message contracts, EventManager.Models.ClearCacheEvent, IndexCompletedEvent, PrepareElasticDataEvent, SyncPriceToSearchEvent, SyncProductDeleteToSearchEvent, SyncProductUpsertToSearchEvent, UpdateVariantElasticPublishedEvent
  • nothing else matches, so no false pair to weigh against them

Dropping the namespace from the key is what makes this dangerous, and it fails immediately: both services carry their own Settings, Configuration and similar short names. Namespace agreement is doing the work.

Proposed shape

A pass after compose in the merge-graphs path that adds an edge between sourced type declarations agreeing on namespace and name and coming from different repos:

p_CatalogService::...event  --same_type_as-->  p_ElasticsearchService::...event

Edges, not node merging. Two repos can hold copies of a contract that have drifted, and collapsing them hides that, while a link lets a traversal cross with each side keeping its own members, file and provenance. That also keeps this out of #296 and #207's territory, which are about merging duplicates rather than relating them.

On the pair above it adds 7 edges and the producer-to-consumer walk becomes three hops:

.UpdateProductOnSearchAppAsync()  (CatalogService)
  -> SyncProductUpsertToSearchEvent  (CatalogService)
  -> SyncProductUpsertToSearchEvent  (ElasticsearchService)   [same_type_as]
  -> ProductUpsertConsumer           (ElasticsearchService)

One caveat worth stating: the last hop is a reverse traversal, because the consumer references the event rather than the other way round, so this shows up under an undirected walk. That direction is inherent to references and not something this pass changes.

How this differs from #2134

Cluster graphs there model cross-repo links declared by hand in cluster.json, file to file. That is the right tool when the connection is an API call with no shared symbol. A shared contract type needs no declaration: both repos already name it, so the join can be derived. The two compose rather than compete, and if cluster graphs land, the same pass belongs in cluster build too.

Related

Patch with tests coming against v8.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions