DICOM Basics using .NET and C# - Understanding DICOMweb

Introduction

This is part of my series of articles on the DICOM standard. In this tutorial, we'll explore DICOMweb, a set of RESTful web services for accessing DICOM objects over HTTP/HTTPS. DICOMweb provides a modern, web-friendly alternative to traditional DICOM DIMSE network protocols.

DICOMweb is particularly valuable for building web-based medical imaging applications, mobile viewers, and cloud-based solutions. It enables standard HTTP tools and libraries to interact with DICOM servers without requiring specialized DICOM networking implementations.

Prerequisites

Before you begin, ensure you have the following:

  • A .NET development environment (Visual Studio or Visual Studio Code)
  • A DICOMweb-capable server (Orthanc with DICOMweb plugin or DCM4CHEE)
  • Basic understanding of REST APIs and HTTP
  • You can find all the code demonstrated in this tutorial on GitHub here

“The Internet is becoming the town square for the global village of tomorrow.” ~ Bill Gates

The Theory Behind DICOMweb

Traditional DICOM networking was designed in the early 1990s when healthcare facilities had isolated networks and dedicated imaging equipment. The DIMSE protocol uses persistent associations, custom port numbers, and binary encoding - all barriers to integration with modern web infrastructure. DICOMweb represents a paradigm shift: reimagining DICOM access using the architectural principles that made the World Wide Web successful.

REST (Representational State Transfer) is the architectural style underlying HTTP. Resources are identified by URLs, manipulated through standard verbs (GET, POST), and represented in negotiated formats. DICOMweb maps DICOM operations to REST: studies, series, and instances become URL-addressable resources. GET retrieves them, POST stores them. This alignment means standard web tools - browsers, HTTP libraries, API gateways - work natively with DICOM data.

The three services (QIDO-RS, WADO-RS, STOW-RS) mirror the core DICOM operations: C-FIND becomes QIDO queries with URL parameters, C-MOVE/C-GET become WADO retrieval via GET requests, and C-STORE becomes STOW uploads via POST. The mapping isn't perfect (DIMSE has capabilities without direct REST equivalents), but covers the 80% use case elegantly.

For cloud-native architectures, DICOMweb is transformative. Load balancers, CDNs, API gateways, and container orchestration all understand HTTP natively. You can cache frequently accessed studies, geographically distribute DICOM data through CDN edge nodes, apply standard authentication (OAuth2) and authorization, and monitor traffic with standard observability tools. None of this works easily with DIMSE's custom protocol.

The JSON representation of DICOM metadata (application/dicom+json) enables frontend integration that was previously impractical. JavaScript in a browser can fetch study metadata, parse JSON natively, and render study lists without specialized DICOM libraries. This democratizes DICOM development, allowing web developers to build imaging applications without deep DICOM expertise.

DICOMweb Services Overview

DICOMweb defines three core RESTful services:

ServiceMethodPurpose
QIDO-RSGETQuery based on ID for DICOM Objects - Search
WADO-RSGETWeb Access to DICOM Objects - Retrieve
STOW-RSPOSTStore Over the Web - Upload

QIDO-RS: Query Service

QIDO-RS enables searching for studies, series, or instances using query parameters:

EndpointDescription
/studiesSearch all studies
/studies/{studyUID}/seriesSearch series in a study
/studies/{studyUID}/series/{seriesUID}/instancesSearch instances

Common query parameters:

ParameterDescription
PatientNamePatient name (wildcards allowed)
PatientIDPatient identifier
StudyDateStudy date or range
ModalityModality type (CT, MR, US, etc.)
limitMaximum results to return
offsetSkip first N results

WADO-RS: Retrieve Service

WADO-RS enables retrieving DICOM objects and metadata:

EndpointReturns
/studies/{studyUID}All instances in study
/studies/{studyUID}/series/{seriesUID}All instances in series
/studies/{studyUID}/series/{seriesUID}/instances/{instanceUID}Single instance
/studies/{studyUID}/metadataStudy metadata (JSON)
…/renderedRendered image (JPEG/PNG)

STOW-RS: Store Service

STOW-RS enables uploading DICOM objects:

EndpointMethodContent-Type
/studiesPOSTmultipart/related; type=“application/dicom”
/studies/{studyUID}POSTAdd to specific study

Step 1 of 3: Querying with QIDO-RS

Let's implement QIDO-RS queries using HttpClient:

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace DICOMwebExample
{
    public class Program
    {
        private static readonly string DicomWebBaseUrl = "http://localhost:8042/dicom-web";

        public static async Task Main(string[] args)
        {
            Console.WriteLine("=== DICOMweb Services Tutorial ===");

            using (var httpClient = new HttpClient())
            {
                httpClient.BaseAddress = new Uri(DicomWebBaseUrl);

                // QIDO-RS: Query for Studies
                await QueryStudiesAsync(httpClient);

                // WADO-RS: Retrieve example URLs
                DisplayWadoRsExamples();

                // STOW-RS: Store example
                DisplayStowRsExample();
            }
        }
    }
}

QIDO-RS Query Implementation

private static async Task QueryStudiesAsync(HttpClient httpClient)
{
    Console.WriteLine("--- QIDO-RS: Querying for Studies ---");

    try
    {
        // Query for all studies (limit to 10)
        var qidoUrl = "/studies?limit=10";
        Console.WriteLine($"  GET {DicomWebBaseUrl}{qidoUrl}");

        // Set Accept header for JSON response
        httpClient.DefaultRequestHeaders.Accept.Clear();
        httpClient.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/dicom+json"));

        var response = await httpClient.GetAsync(qidoUrl);

        if (response.IsSuccessStatusCode)
        {
            var jsonContent = await response.Content.ReadAsStringAsync();
            Console.WriteLine($"  Status: {response.StatusCode}");
            Console.WriteLine($"  Content-Type: {response.Content.Headers.ContentType}");
            Console.WriteLine($"  Response: {jsonContent.Substring(0, Math.Min(500, jsonContent.Length))}...");
        }
        else
        {
            Console.WriteLine($"  Query failed: {response.StatusCode}");
        }
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"  Connection failed: {ex.Message}");
        Console.WriteLine("  Ensure Orthanc with DICOMweb plugin is running.");
    }
}

Step 2 of 3: Retrieving with WADO-RS

WADO-RS supports multiple content types for different use cases:

private static void DisplayWadoRsExamples()
{
    Console.WriteLine("--- WADO-RS: Retrieve Examples ---");

    // Retrieve entire study as DICOM
    Console.WriteLine("  Retrieve study (DICOM):");
    Console.WriteLine($"    GET {DicomWebBaseUrl}/studies/{{studyUID}}");
    Console.WriteLine("    Accept: multipart/related; type=\"application/dicom\"");

    // Retrieve study metadata as JSON
    Console.WriteLine("  Retrieve metadata (JSON):");
    Console.WriteLine($"    GET {DicomWebBaseUrl}/studies/{{studyUID}}/metadata");
    Console.WriteLine("    Accept: application/dicom+json");

    // Retrieve rendered image
    Console.WriteLine("  Retrieve rendered image:");
    Console.WriteLine($"    GET {DicomWebBaseUrl}/studies/{{studyUID}}/series/{{seriesUID}}/instances/{{instanceUID}}/rendered");
    Console.WriteLine("    Accept: image/jpeg or image/png");

    // Retrieve specific frames
    Console.WriteLine("  Retrieve specific frame:");
    Console.WriteLine($"    GET .../instances/{{instanceUID}}/frames/1");
}

private static async Task RetrieveRenderedImageAsync(HttpClient httpClient,
    string studyUid, string seriesUid, string instanceUid)
{
    var url = $"/studies/{studyUid}/series/{seriesUid}/instances/{instanceUid}/rendered";

    httpClient.DefaultRequestHeaders.Accept.Clear();
    httpClient.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("image/jpeg"));

    var response = await httpClient.GetAsync(url);

    if (response.IsSuccessStatusCode)
    {
        var imageBytes = await response.Content.ReadAsByteArrayAsync();
        Console.WriteLine($"  Retrieved image: {imageBytes.Length} bytes");

        // Save to file
        await File.WriteAllBytesAsync("rendered_image.jpg", imageBytes);
    }
}

Step 3 of 3: Storing with STOW-RS

STOW-RS uploads DICOM objects using multipart content:

private static void DisplayStowRsExample()
{
    Console.WriteLine("--- STOW-RS: Store Example ---");

    Console.WriteLine("  Store DICOM file:");
    Console.WriteLine($"    POST {DicomWebBaseUrl}/studies");
    Console.WriteLine("    Content-Type: multipart/related; type=\"application/dicom\"");
}

private static async Task StoreDicomFileAsync(HttpClient httpClient, string dicomFilePath)
{
    Console.WriteLine("--- STOW-RS: Storing DICOM File ---");

    // Read DICOM file
    var dicomBytes = await File.ReadAllBytesAsync(dicomFilePath);

    // Create multipart content
    using var content = new MultipartContent("related");
    content.Headers.ContentType.Parameters.Add(
        new NameValueHeaderValue("type", "\"application/dicom\""));

    var dicomContent = new ByteArrayContent(dicomBytes);
    dicomContent.Headers.ContentType = new MediaTypeHeaderValue("application/dicom");
    content.Add(dicomContent);

    // Send POST request
    var response = await httpClient.PostAsync("/studies", content);

    if (response.IsSuccessStatusCode)
    {
        var result = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"  Store successful: {response.StatusCode}");
        Console.WriteLine($"  Response: {result}");
    }
    else
    {
        Console.WriteLine($"  Store failed: {response.StatusCode}");
    }
}

Content Types

DICOMweb uses specific content types:

Content TypePurpose
application/dicom+jsonDICOM metadata as JSON
multipart/related; type=“application/dicom”DICOM objects
image/jpegRendered JPEG images
image/pngRendered PNG images
application/pdfEncapsulated PDF documents

Orthanc DICOMweb Configuration

To use DICOMweb with Orthanc, enable the plugin in your configuration:

{
  "Plugins": ["libOrthancDicomWeb.so"],
  "DicomWeb": {
    "Enable": true,
    "Root": "/dicom-web/"
  }
}

Default endpoints:

  • Orthanc: http://localhost:8042/dicom-web/
  • DCM4CHEE: http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs/

Best Practices

  • Use HTTPS: Always use HTTPS in production for patient data security
  • Authentication: Implement OAuth2 or similar for access control
  • Pagination: Use limit and offset for large result sets
  • Caching: Leverage HTTP caching for metadata and rendered images
  • Compression: Request gzip compression for large responses

Advantages of DICOMweb

  • Uses standard HTTP/HTTPS protocols
  • Works with standard web development tools
  • Easier to integrate with web applications
  • Firewall-friendly (standard ports)
  • Supports modern authentication (OAuth2)
  • JSON metadata for easy parsing

Conclusion

DICOMweb provides a modern, RESTful approach to accessing DICOM data. With QIDO-RS for querying, WADO-RS for retrieving, and STOW-RS for storing, you have all the building blocks needed to create web-based medical imaging applications.

While traditional DICOM DIMSE protocols remain important for modality integration and legacy systems, DICOMweb is ideal for building cloud-native applications, web viewers, and mobile solutions that need to access DICOM data over the internet.

Please check out the next tutorial in this series where we cover DICOM Transfer Syntax and compression.