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:
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.
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.
ResolveAsync(string reference) — return the descriptor a reference points to. Throw if not tagged.
TagAsync(Descriptor, string reference) — point the tag at a descriptor already in the store. Throw if the descriptor isn't present (mirrors Content.MemoryStore).
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.
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.
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
- 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.
- 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.
- 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?
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 ofOrasProject.Oras.Content.MemoryStore. Naming up to maintainers — suggestedOrasProject.Oras.Registry.Memory.MemoryRepositoryorInMemoryRepository.A first-cut surface that would unblock the common test scenarios:
PushAsync(Descriptor, Stream)andPushAsync(Descriptor, Stream, string reference)— store bytes keyed by digest. The reference overload also creates the tag mapping atomically (no separateTagAsynccall required), matching the distribution-specPUT /v2/<name>/manifests/<reference>semantic.FetchAsync(string reference)andFetchAsync(Descriptor)— resolve tag → digest if a string reference is passed, return aMemoryStreamover the stored bytes. ThrowNotFoundExceptionif missing.ResolveAsync(string reference)— return the descriptor a reference points to. Throw if not tagged.TagAsync(Descriptor, string reference)— point the tag at a descriptor already in the store. Throw if the descriptor isn't present (mirrorsContent.MemoryStore).Blobs/Manifestssubstores — returnIBlobStore/IManifestStoreviews over the same underlying bytes. Internally these can share storage; the split exists so consumers can target the right substore in tests.FetchReferrersAsync(Descriptor, string? artifactType)— return all pushed manifests whoseSubjectmatches the given descriptor. Built up incrementally as manifests are pushed, or computed on demand by scanning stored manifests.ListTagsAsync/DeleteAsync/MountAsync/GetEndpointAsync(IBlobLocationProvider) —NotSupportedExceptionis 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
Content.MemoryStoreand this new type. They serve different purposes and the CAS/registry distinction is correct.IReferenceFetchable/IReferencePushabletoContent.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, andGET /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.IRepositorysurface.Alternatives considered
IReferenceFetchable/IReferencePushablegiving Resolve+Fetch / Push+Tag implementations. Rejected: same atomicity-misleading concern as bolting them ontoMemoryStore, plus it would surprise existingIRepositoryimplementers who deliberately override these methods.OrasProject.Oras.TestHelperspackage. Acceptable if maintainers want to keep the main package lean — but the helper has to take a public dependency onIRepositoryeither way, which the maintainers control. The main SDK package seems like the right home.Open questions for maintainers
MemoryRepository,InMemoryRepository,Registry.Memory.Repository?ITargetfor 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).NotSupportedExceptionacceptable forIMounter/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:
org.opencontainers.image.createdannotation.artifactTypeand layer annotations.Today the SDK ships
Content.MemoryStore : ITarget, which is great for CAS-level tests. But our production code is written againstRegistry.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 onIRepositoryand 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
IOciRepositoryinterface with two implementations:Registry.Remote.Repository.Content.MemoryStorewith 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
IRepositoryforces every downstream project to update its hand-rolled adapter. Centralizing this in the SDK means the SDK's own API evolution is self-validating — ifMemoryRepositorydoesn't compile after a change, that's a useful signal.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., assumingFetchAsync(reference)does Resolve+Fetch in two calls when on a remote it's an atomic GET; assumingPushAsync(desc, stream, ref)is equivalent toPushthenTagwhen on a registry it's atomic). Shipping a real in-memory implementation eliminates that drift.oras-goshipsmemory.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/IReferencePushabletoContent.MemoryStore". I don't think that's right:Contentnamespace is for generic CAS stores. TheRegistrynamespace is for distribution-spec targets with manifest/blob CAS separation, referrer APIs, mounting, blob locations. That layering is intentional and correct.MemoryStoreas "Resolve+Fetch" / "Push+Tag" would advertise registry-like behavior the CAS can't honor.MemoryStoreasIRepository-substitutable, then trip over the missingBlobs/Manifests/FetchReferrersAsync/ mount / tag-listing surfaces.The right shape is a new type that owns the registry-shaped contract end-to-end, leaving
MemoryStoreas a pure CAS.Are you willing to submit PRs to contribute to this feature?