Skip to content

Add an in-memory IRepository implementation for tests #386

Description

@ridhoq

What is the version or commit of the ORAS .NET library?

v0.5.0

What would you like to be added?

What would you like to be added?

A new in-memory implementation of OrasProject.Oras.Registry.IRepository, shipped in the SDK, intended as the registry-shaped peer of OrasProject.Oras.Content.MemoryStore. Naming up to maintainers — suggested OrasProject.Oras.Registry.Memory.MemoryRepository or InMemoryRepository.

A first-cut surface that would unblock the common test scenarios:

  1. PushAsync(Descriptor, Stream) and PushAsync(Descriptor, Stream, string reference) — store bytes keyed by digest. The reference overload also creates the tag mapping atomically (no separate TagAsync call required), matching the distribution-spec PUT /v2/<name>/manifests/<reference> semantic.
  2. FetchAsync(string reference) and FetchAsync(Descriptor) — resolve tag → digest if a string reference is passed, return a MemoryStream over the stored bytes. Throw NotFoundException if missing.
  3. ResolveAsync(string reference) — return the descriptor a reference points to. Throw if not tagged.
  4. TagAsync(Descriptor, string reference) — point the tag at a descriptor already in the store. Throw if the descriptor isn't present (mirrors Content.MemoryStore).
  5. Blobs / Manifests substores — return IBlobStore / IManifestStore views over the same underlying bytes. Internally these can share storage; the split exists so consumers can target the right substore in tests.
  6. FetchReferrersAsync(Descriptor, string? artifactType) — return all pushed manifests whose Subject matches the given descriptor. Built up incrementally as manifests are pushed, or computed on demand by scanning stored manifests.
  7. ListTagsAsync / DeleteAsync / MountAsync / GetEndpointAsync (IBlobLocationProvider)NotSupportedException is acceptable for a first cut; mark as future work in XML doc comments. Tests that don't exercise them aren't affected.

A test-friendly registry doesn't need to enforce every distribution-spec edge case (manifest size limits, exhaustive content-type validation, exact 4xx error mappings) — those belong in the remote Registry.Remote.Repository. The goal is "pushes round-trip, references resolve, referrers chain works" — enough for downstream consumers to test their orchestration code without mocking SDK interfaces.

Explicit non-goals

  • Not asking to merge Content.MemoryStore and this new type. They serve different purposes and the CAS/registry distinction is correct.
  • Not asking to add IReferenceFetchable / IReferencePushable to Content.MemoryStore. On a real registry those overloads aren't pure sugar over CAS operations — PUT /v2/<name>/manifests/<reference> sets the tag atomically as a side effect of the manifest PUT, and GET /v2/<name>/manifests/<reference> is one round trip vs. Resolve(HEAD) + Fetch(GET) with a documented TOCTOU race that registry implementations must reason about. Advertising those interfaces on a CAS would mislead callers about atomicity.
  • Not asking to change the public IRepository surface.

Alternatives considered

  • Default interface methods on IReferenceFetchable / IReferencePushable giving Resolve+Fetch / Push+Tag implementations. Rejected: same atomicity-misleading concern as bolting them onto MemoryStore, plus it would surprise existing IRepository implementers who deliberately override these methods.
  • A separate OrasProject.Oras.TestHelpers package. Acceptable if maintainers want to keep the main package lean — but the helper has to take a public dependency on IRepository either way, which the maintainers control. The main SDK package seems like the right home.
  • Document the gap and tell consumers to write their own. This is the status quo and produces the drift described in the next section.

Open questions for maintainers

  • Naming — MemoryRepository, InMemoryRepository, Registry.Memory.Repository?
  • Should the new type also be usable as an ITarget for tests that bridge CAS and registry code? Implementing both interfaces on one type is fine if the semantics are consistent (push-by-descriptor shares the backing store with push-with-reference).
  • Is NotSupportedException acceptable for IMounter / IBlobLocationProvider / tag listing in v1, or should those be implemented from day one?

Why is this needed for the ORAS .NET library?

User story

A team building an OCI-artifact-backed publishing pipeline needs unit tests that exercise:

  • Deterministic manifest composition — same input must produce the same digest, no auto-injected org.opencontainers.image.created annotation.
  • Push/fetch round-trips for OCI 1.1 manifests with custom artifactType and layer annotations.
  • Repeat-push dedup — confirming that ORAS's HEAD-before-write behavior on blob pushes results in wire-level no-ops cycle-over-cycle.
  • Referrer-chain traversal for artifacts attached to subject manifests.

Today the SDK ships Content.MemoryStore : ITarget, which is great for CAS-level tests. But our production code is written against Registry.IRepository — the surface that real consumers code against. The reference-overload methods (IReferenceFetchable.FetchAsync(string), IReferencePushable.PushAsync(Descriptor, Stream, string)) and the substores (Blobs, Manifests) live on IRepository and have no in-memory counterpart in the SDK.

Current workaround

Every downstream project ends up hand-rolling an adapter. In one such case (a ~300-LOC OCI artifact library that wraps ORAS for a tagged-manifest push pipeline), the workaround is a narrow internal IOciRepository interface with two implementations:

  • A "remote" adapter wrapping Registry.Remote.Repository.
  • An "in-memory" adapter wrapping Content.MemoryStore with manual Resolve+Fetch and Push+Tag composition to fake the reference-* methods.

The wrap-the-CAS-as-a-registry adapter is ~80 LOC and exists only because the SDK doesn't ship one. This is a recurring pattern across consumers — every team that takes testing seriously reinvents the same wheel.

Why this belongs in the SDK and not downstream

  1. Maintenance cost compounds across consumers. Every breaking interface change in IRepository forces every downstream project to update its hand-rolled adapter. Centralizing this in the SDK means the SDK's own API evolution is self-validating — if MemoryRepository doesn't compile after a change, that's a useful signal.
  2. Mocking produces broken tests. Without an in-memory IRepository, the path of least resistance for consumers is mocking the ORAS interfaces directly — which the repo's own copilot-instructions discourage. Mock-based tests routinely encode broken assumptions about ORAS semantics (e.g., assuming FetchAsync(reference) does Resolve+Fetch in two calls when on a remote it's an atomic GET; assuming PushAsync(desc, stream, ref) is equivalent to Push then Tag when on a registry it's atomic). Shipping a real in-memory implementation eliminates that drift.
  3. Prior art. oras-go ships memory.New() returning a registry-shaped in-memory target. oras-dotnet's absence is a visible gap.

Why bolt-on-to-MemoryStore is the wrong fix

A natural-looking alternative is "just add IReferenceFetchable / IReferencePushable to Content.MemoryStore". I don't think that's right:

  • The Content namespace is for generic CAS stores. The Registry namespace is for distribution-spec targets with manifest/blob CAS separation, referrer APIs, mounting, blob locations. That layering is intentional and correct.
  • The reference overloads aren't sugar over CAS operations on a real registry — they map to distinct distribution-spec endpoints with atomicity and TOCTOU semantics a CAS doesn't have. Implementing them on MemoryStore as "Resolve+Fetch" / "Push+Tag" would advertise registry-like behavior the CAS can't honor.
  • Consumers would start treating MemoryStore as IRepository-substitutable, then trip over the missing Blobs / Manifests / FetchReferrersAsync / mount / tag-listing surfaces.

The right shape is a new type that owns the registry-shaped contract end-to-end, leaving MemoryStore as a pure CAS.

Are you willing to submit PRs to contribute to this feature?

  • Yes, I am willing to implement it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttriageNew issues or PRs to be acknowledged by maintainers

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions