Standard Services

Standard services and provider contracts

Standard handlers are the simplest server starting point when their persistence contracts fit your application. They implement transaction validation, DICOM search matching and projection, response formatting, and relevant workflow rules. They do not install a database, archive, renderer, or subscription store. Implement the provider interfaces against your own infrastructure. Asynchronous provider I/O methods accept a cancellation token; forward it to backend I/O and stop asynchronous enumeration when cancellation is requested. Synchronous capability methods such as GetResponseMediaTypes do not take a token.

Register on DicomWebApplication.CreateBuilder() or use the matching DicomWebAspNetCoreBuilder.AddStandard...() method. The latter resolves providers from each ASP.NET Core application scope. DicomWebStandardServiceOptions.MaximumSearchResults defaults to 1000 and must be positive. It caps results returned by the standard search engines even when a request asks for more. A request’s limit and offset still control its page; backend candidates must be in stable order so repeated pages are meaningful.

Registration Required provider Optional provider Transactions supplied
AddStandardStudiesService IDicomWebStudiesProvider, IDicomWebRepresentationProvider in the current source Options Study, series, and instance search, store, metadata, DICOM and bulk/frame retrieval, plus WADO-URI DICOM retrieval. Rendered routes, including WADO-URI rendered retrieval and thumbnails, depend on declared representation media types. The standard handler does not implement MPR or 3D volume rendering.
AddStandardWorklistService IDicomWebWorkitemProvider IDicomWebSubscriptionProvider, options Create, retrieve, update, change state, search; cancellation request, subscriptions, and notification connection when subscriptions are supplied; filtered subscription routes when the provider declares support
AddStandardStorageCommitmentService IDicomWebStorageCommitmentProvider — Request and check result
AddStandardModalityWorklistService IDicomWebModalityWorklistProvider Options Search scheduled procedure steps
AddStandardModalityPerformedProcedureStepService IDicomWebMppsProvider — Create, update, retrieve performed procedure steps
AddStandardNonPatientInstanceService IDicomWebNonPatientInstanceProvider Options Search, store, retrieve non-patient instances

The current AddStandardStudiesService source dereferences representations.SupportedMediaTypes during registration. Supply a representation provider, even if its supported media type map is empty. Its default RetrieveAsync can render simple JPEG frames; controls such as viewport, window, and ICC profile, and whole multi-frame rendering require an application renderer. Advertise only combinations your provider can produce.

The current standard representation handler throws NotSupportedException for every MPR and 3D volume-rendering transaction, even if its provider declares a media type. Do not advertise those transaction keys with the standard handler. A replacement custom handler can implement a selected transaction; it must supply its own rendering and response policy. An uncaught NotSupportedException follows the host exception pipeline rather than becoming a DICOMweb error response.

Search providers

IDicomWebStudiesProvider.FindCandidatesAsync, IDicomWebWorkitemProvider.FindCandidatesAsync, IDicomWebModalityWorklistProvider.FindCandidatesAsync, and IDicomWebNonPatientInstanceProvider.FindCandidatesAsync return possible matches, one dataset per resource at DicomWebSearchContext.Level, in stable order. The context also gives the URI’s parent Resource and compiled Plan. A backend may return a superset. The standard engine applies final Plan.Matches, Plan.Project, offset, and limit; filtering a true match out in the backend cannot be repaired by that final pass. Plan.IncludedAttributes and IncludeAllAttributes describe projection, while Offset and Limit describe requested pagination.

Do not apply Plan.Offset or Plan.Limit to the provider’s candidate stream: the standard engine applies them after matching, so backend pagination would skip or truncate results again. Keep attributes needed for matching in each candidate until that pass has run. MaximumSearchResults bounds the response size; it does not bound how many candidates are read. The engine enumerates the full candidate stream to count remaining matches and adds a Warning response header when more results are available. Push supported matching predicates into the backend to reduce that work without discarding true matches.

Compiled search filters

Plan.Filter is a DicomWebSearchFilter. Its Matches property exposes the predicate tree to use when translating a search into a database query. All nodes in one match collection must succeed against the same dataset. An empty collection matches every dataset.

Match node Meaning at the current dataset scope
DicomWebSearchMatch Base type for a node; Tag identifies the attribute examined at this scope.
DicomWebCriterionMatch Applies its Criterion to that attribute. DicomWebSearchCriterion.Attribute retains the full DicomWebAttributePath, Kind identifies the matching rule, and Values contains decoded matching values.
DicomWebSequenceMatch Requires at least one item of the sequence identified by Tag to satisfy all its ItemMatches. Nested sequence nodes apply the same rule recursively. A missing attribute, a non-sequence attribute, or a sequence with no matching item fails this node.

For example, the search keys RequestAttributesSequence.ScheduledProcedureStepID=STEP and RequestAttributesSequence.RequestedProcedureID=PROCEDURE produce one sequence node with two criterion nodes:

RequestAttributesSequence: at least one item must satisfy both
  ScheduledProcedureStepID = STEP
  RequestedProcedureID = PROCEDURE

A sequence containing (STEP, OTHER) in one item and (OTHER, PROCEDURE) in another does not match. A sequence containing (STEP, PROCEDURE) in one item does match. A database translation therefore needs one correlated existence check for the sequence item, with both conditions inside it; separate existence checks could select different items.

Enumerating Plan.Filter directly yields a flat sequence of DicomWebSearchCriterion values from the tree. This is useful for inspecting attribute paths and selecting safe backend predicates, but it does not expose the sequence nodes or their item scopes. Use Filter.Matches and DicomWebSequenceMatch.ItemMatches when preserving those scopes in a backend query. If the backend cannot reproduce a matching rule, return a broader candidate set and let Plan.Matches make the final decision. The library’s final matcher is authoritative.

Search provider examples

The following provider is complete for a read-only, empty modality worklist. Replace its enumeration with a stable backend query when scheduled steps exist:

public sealed class EmptyModalityWorklist : Aspose.Medical.Dicom.Web.Providers.IDicomWebModalityWorklistProvider
{
    public async System.Collections.Generic.IAsyncEnumerable<Aspose.Medical.Dicom.Dataset> FindCandidatesAsync(
        Aspose.Medical.Dicom.Web.Providers.DicomWebSearchContext search,
        [System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        await System.Threading.Tasks.Task.CompletedTask;
        yield break;
    }
}

For Studies, this small in-memory archive demonstrates all three required storage methods. It keeps caller-provided DicomFile objects in process memory and loses them on restart. Replace it with durable storage and database-backed candidate selection for a real service:

public sealed class InMemoryStudies : Aspose.Medical.Dicom.Web.Providers.IDicomWebStudiesProvider
{
    private readonly System.Collections.Concurrent.ConcurrentDictionary<string, Aspose.Medical.Dicom.DicomFile> files = new();

    public System.Threading.Tasks.ValueTask<Aspose.Medical.Dicom.Web.Providers.DicomWebStoreInstanceResult> StoreAsync(
        Aspose.Medical.Dicom.DicomFile instance, System.Threading.CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        string uid = ReadUid(instance.Dataset, Aspose.Medical.Dicom.Tags.Tag.SOPInstanceUID);
        bool added = files.TryAdd(uid, instance);
        Aspose.Medical.Dicom.Web.Providers.DicomWebStoreInstanceResult result = new()
        {
            Outcome = added ? Aspose.Medical.Dicom.Web.Providers.DicomWebStoreOutcome.Stored
                            : Aspose.Medical.Dicom.Web.Providers.DicomWebStoreOutcome.Failed
        };
        return System.Threading.Tasks.ValueTask.FromResult(result);
    }

    public async System.Collections.Generic.IAsyncEnumerable<Aspose.Medical.Dicom.DicomFile> RetrieveAsync(
        Aspose.Medical.Dicom.Web.Providers.DicomWebResourceIdentifier resource,
        [System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken = default)
    {
        foreach (Aspose.Medical.Dicom.DicomFile file in Select(resource))
        {
            cancellationToken.ThrowIfCancellationRequested();
            yield return file;
        }
        await System.Threading.Tasks.Task.CompletedTask;
    }

    public async System.Collections.Generic.IAsyncEnumerable<Aspose.Medical.Dicom.Dataset> FindCandidatesAsync(
        Aspose.Medical.Dicom.Web.Providers.DicomWebSearchContext search,
        [System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken = default)
    {
        System.Collections.Generic.HashSet<string> seen = new();
        foreach (Aspose.Medical.Dicom.DicomFile file in Select(search.Resource))
        {
            cancellationToken.ThrowIfCancellationRequested();
            Aspose.Medical.Dicom.Dataset dataset = file.Dataset;
            string study = ReadUid(dataset, Aspose.Medical.Dicom.Tags.Tag.StudyInstanceUID);
            string series = ReadUid(dataset, Aspose.Medical.Dicom.Tags.Tag.SeriesInstanceUID);
            string instance = ReadUid(dataset, Aspose.Medical.Dicom.Tags.Tag.SOPInstanceUID);
            string key = search.Level switch
            {
                Aspose.Medical.Dicom.Web.Providers.DicomWebSearchLevel.Study => study,
                Aspose.Medical.Dicom.Web.Providers.DicomWebSearchLevel.Series => study + "/" + series,
                _ => study + "/" + series + "/" + instance
            };
            if (seen.Add(key))
                yield return dataset;
        }
        await System.Threading.Tasks.Task.CompletedTask;
    }

    private System.Collections.Generic.IEnumerable<Aspose.Medical.Dicom.DicomFile> Select(
        Aspose.Medical.Dicom.Web.Providers.DicomWebResourceIdentifier resource)
    {
        string? study = resource.StudyInstanceUid;
        string? series = resource.SeriesInstanceUid;
        string? instance = resource.SopInstanceUid;
        System.Collections.Generic.List<Aspose.Medical.Dicom.DicomFile> selected = new();
        foreach (Aspose.Medical.Dicom.DicomFile file in files.Values)
        {
            if (!string.IsNullOrEmpty(study) && ReadUid(file.Dataset, Aspose.Medical.Dicom.Tags.Tag.StudyInstanceUID) != study)
                continue;
            if (!string.IsNullOrEmpty(series) && ReadUid(file.Dataset, Aspose.Medical.Dicom.Tags.Tag.SeriesInstanceUID) != series)
                continue;
            if (!string.IsNullOrEmpty(instance) && ReadUid(file.Dataset, Aspose.Medical.Dicom.Tags.Tag.SOPInstanceUID) != instance)
                continue;
            selected.Add(file);
        }
        selected.Sort((left, right) =>
        {
            string leftKey = ReadUid(left.Dataset, Aspose.Medical.Dicom.Tags.Tag.StudyInstanceUID) + "/" +
                             ReadUid(left.Dataset, Aspose.Medical.Dicom.Tags.Tag.SeriesInstanceUID) + "/" +
                             ReadUid(left.Dataset, Aspose.Medical.Dicom.Tags.Tag.SOPInstanceUID);
            string rightKey = ReadUid(right.Dataset, Aspose.Medical.Dicom.Tags.Tag.StudyInstanceUID) + "/" +
                              ReadUid(right.Dataset, Aspose.Medical.Dicom.Tags.Tag.SeriesInstanceUID) + "/" +
                              ReadUid(right.Dataset, Aspose.Medical.Dicom.Tags.Tag.SOPInstanceUID);
            return System.StringComparer.Ordinal.Compare(leftKey, rightKey);
        });
        return selected;
    }

    private static string ReadUid(Aspose.Medical.Dicom.Dataset dataset, Aspose.Medical.Dicom.Tags.Tag tag) =>
        dataset.GetSingleValueOrDefault(tag, string.Empty) ?? string.Empty;
}

public sealed class NoRenderedRepresentations : Aspose.Medical.Dicom.Web.Providers.IDicomWebRepresentationProvider
{
    public System.Collections.Immutable.ImmutableDictionary<Aspose.Medical.Dicom.Web.DicomWebTransaction,
        System.Collections.Immutable.ImmutableArray<string>> SupportedMediaTypes { get; } =
        ImmutableDictionary<DicomWebTransaction,
            ImmutableArray<string>>.Empty;

    public System.Collections.Immutable.ImmutableArray<string> GetResponseMediaTypes(
        Aspose.Medical.Dicom.Web.Providers.DicomWebRepresentationRequest request) => [];
}

Register both objects on the hosting-neutral builder:

Aspose.Medical.Dicom.Web.DicomWebApplication application =
    DicomWebApplication.CreateBuilder()
        .AddStandardStudiesService(new InMemoryStudies(), new NoRenderedRepresentations())
        .Build();

In ASP.NET Core, register both provider interfaces in DI and call the adapter builder’s AddStandardStudiesService(). An empty representation declaration leaves application-specific rendered routes out of capabilities. The example returns one instance dataset as the study/series candidate; an archive should construct level-appropriate datasets and consistent search attributes rather than rely on this simplification.

Persistence and representation contracts

Interface Members and application obligation
IDicomWebStudiesProvider FindCandidatesAsync supplies study/series/instance candidates; StoreAsync(DicomFile) persists one validated Part 10 instance and returns DicomWebStoreInstanceResult; RetrieveAsync(DicomWebResourceIdentifier) streams files in stable study/series/instance order. The resource carries optional Study, Series, and SOP Instance UIDs.
IDicomWebRepresentationProvider SupportedMediaTypes declares transaction to single-part media types. GetResponseMediaTypes(DicomWebRepresentationRequest) reports what a particular request can produce. RetrieveAsync(request, selectMediaType, token) returns DicomWebRepresentationResult with selected media type, headers, and ordered DicomWebEncodedRepresentation items. A delegate selects among offered media types before encoding. The application performs multipart packaging.
IDicomWebNonPatientInstanceProvider FindCandidatesAsync supplies category-specific candidates; StoreAsync(category, DicomFile) reports each store outcome; RetrieveAsync(category, sopInstanceUid) returns the file or null.
IDicomWebWorkitemProvider FindCandidatesAsync supplies UPS candidates; GetAsync(uid) returns DicomWebVersionedDataset or null; TryCreateAsync atomically creates an unused UID; TryReplaceAsync(uid, expectedRevision, completeReplacement) atomically changes exactly that revision. Return false on conflict. Persist the dataset and opaque revision consistently.
IDicomWebMppsProvider The same GetAsync, atomic TryCreateAsync, and revision-checked TryReplaceAsync pattern for performed procedure steps. The engine validates MPPS state; the provider supplies durable atomic storage.
IDicomWebStorageCommitmentProvider TryRequestAsync(transactionUid, IReadOnlyList<DicomWebSopReference>) atomically records a new request and returns its pending or final DicomWebStorageCommitment, or null on UID collision. GetAsync returns the current state/result or null for an unknown UID. Safekeeping decisions belong to the backend.
IDicomWebSubscriptionProvider SupportsFilteredWorklistSubscriptions controls filtered routes. UpsertAsync, RemoveAsync, and SuspendAsync maintain subscription state; ResolveRequestingAeTitleAsync maps host identity when needed. PublishAsync, QueueAsync, and PublishStatusAsync enqueue Event Report datasets; OpenNotificationsAsync streams them in delivery order for a requester. The provider owns retention, recipient matching, and delivery semantics.
Provider value type Meaning
DicomWebStoreInstanceResult / DicomWebStoreOutcome Outcome is Stored, Warning, or Failed. Optional Reason is a DICOM warning or failure code; RetrieveUri is an absolute retrieval URI; CoercedAttributes is one Original Attributes Sequence item describing changes made before storage. These values contribute to the per-instance STOW response.
DicomWebVersionedDataset Stored Dataset with a backend-defined opaque Revision; change it on replacement so UPS/MPPS compare-and-replace is atomic.
DicomWebStorageCommitment / DicomWebStorageCommitmentState State is Pending, Complete, or Gone; CommittedInstances, FailedInstances, and optional RetryAfter describe the result and polling delay. DicomWebSopReference carries SOP Class/Instance UIDs, optional Study/Series UIDs, and optional failure reason.
DicomWebSubscription Required Subscriber, optional WorkitemUid and compiled Filter, plus deletion-lock and suspension flags.
DicomWebSubscriptionUpdateResult / DicomWebSubscriptionUpdateOutcome Outcome, IsDeletionLockGranted (default true), and InitialWorkitemUids report the atomic subscription change and initial State Reports to queue.
DicomWebRepresentationRequest / DicomWebRepresentationResult Request, selected Instances, and one-based Frames; response single-part MediaType, ordered encoded Items, and early Headers. Each DicomWebEncodedRepresentation carries content, required ContentLocation, and optional part headers.

Do not share mutable datasets across concurrent requests without your own synchronization. DicomWebWorklistEventPublisher can create and publish the UPS SCP restart and optional going-down status reports through the subscription provider (PublishRestartedAsync and PublishGoingDownAsync); the provider still queues and delivers them.

Store results and partial success

Return Stored or Warning only when the individual instance has been stored; Warning reports a stored instance that needs a warning, such as attribute coercion. Return Failed for an expected per-instance rejection so the handler can include it in the response and continue with other instances. Supply the applicable Reason; omitted warning and failure codes default to 0xB000 and 0xC000, respectively. An exception aborts normal response construction. Earlier successful provider calls are not rolled back by the standard handler, including when a later payload cannot be parsed.

Store result Studies standard handler Non-Patient Instance standard handler
Every instance stored without warnings 200 OK 200 OK
At least one stored instance, with warnings or failures 202 Accepted 200 OK
All instances failed 409 Conflict 409 Conflict

Inspect the response dataset even after a successful HTTP status. ReferencedSOPSequence identifies stored instances and their optional WarningReason; FailedSOPSequence identifies rejected instances and their FailureReason. The typed client’s StoreInstancesResult.Response contains that dataset for successful HTTP responses. For an all-failed 409, use the DicomWebClientException response diagnostics or the raw client response described in the client guide. A Studies request restricted to one study reports a mismatched Study Instance UID as a failed instance with reason 0xA900 before calling the provider.

The ASP.NET Core equivalent for the example is services.AddScoped<IDicomWebModalityWorklistProvider, EmptyModalityWorklist>() plus services.AddDicomWeb().AddStandardModalityWorklistService(); see the complete application. The standard engine can be combined with a custom replacement for a single transaction; see custom handlers.

For complete provider examples beyond Studies and Modality Worklist, continue with UPS and MPPS workflows, storage commitment and non-patient instances, and representation production.