The DICOM Proxy SDK provides comprehensive libraries and tools for integrating with the DICOM Proxy/Router/PACS system. This document covers the core SDK components, integration patterns, and development guidelines.
DicomProxyCore SDK
├── DicomProxyCore.dll (Main library)
├── DicomProxyCore.Models.dll (Data models)
├── DicomProxyCore.Services.dll (Service interfaces)
├── DicomProxyCore.Dicom.dll (DICOM utilities)
├── DicomProxyCore.Commands.dll (Command processors)
└── DicomProxyCore.Imaging.SkiaSharp.dll (Image processing)
<PackageReference Include="DicomProxyCore" Version="1.09.1" />
<PackageReference Include="DicomProxyCore.Models" Version="1.09.1" />
<PackageReference Include="DicomProxyCore.Services" Version="1.09.1" />
# Copy SDK libraries to your project
cp DicomProxyCore*.dll YourProject/libs/
# Add references in your project file
Central configuration management class.
using DicomProxyCore;
// Initialize configuration
Configuration.Setup("C:\\MyApp", false);
var config = Configuration.Instance;
// Access configuration properties
string storagePath = config.StoragePath;
int dicomPort = config.DicomPort;
string aeTitle = config.ThisModality.AE_Title;
// Update configuration
config.DicomPort = 11112;
config.Save();
Main service orchestrator for DICOM proxy functionality.
using DicomProxyCore;
public class MyDicomService
{
private DicomProxyService _proxyService;
public void Initialize()
{
// Set base path and service mode
_proxyService = new DicomProxyService("C:\\MyApp", false);
// Start the service
_proxyService.Start();
}
public void Shutdown()
{
_proxyService?.Stop();
}
}
DICOM Service Class Provider for handling incoming connections.
using DicomProxyCore.CStoreSCP;
using FellowOakDicom.Network;
public class CustomCStoreSCP : CStoreSCP
{
public override DicomCStoreResponse OnCStoreRequest(DicomCStoreRequest request)
{
// Custom C-STORE handling
var dataset = request.Dataset;
var studyUID = dataset.GetSingleValue<string>(DicomTag.StudyInstanceUID);
// Process the DICOM data
ProcessDicomData(dataset);
// Return success response
return new DicomCStoreResponse(request, DicomStatus.Success);
}
private void ProcessDicomData(DicomDataset dataset)
{
// Custom processing logic
var patientName = dataset.GetSingleValueOrDefault(DicomTag.PatientName, "Unknown");
var modality = dataset.GetSingleValueOrDefault(DicomTag.Modality, "");
Console.WriteLine($"Received: {patientName} - {modality}");
}
}
using DicomProxyCore.CStoreSCP.ConnectionManager;
using DicomProxyCore.Models;
public class ConnectionService
{
private readonly FreqUsedPcs _connectionManager;
public ConnectionService()
{
_connectionManager = new FreqUsedPcs();
}
public async Task<bool> TestConnectionAsync(Connection server)
{
try
{
var client = _connectionManager.GetClient(server);
await client.AddRequestAsync(new DicomCEchoRequest());
await client.SendAsync();
return true;
}
catch
{
return false;
}
}
}
using DicomProxyCore.Models;
var server = new Connection
{
Id = "pacs1",
Name = "Main PACS Server",
AE_Title = "MAINPACS",
Hostname = "192.168.1.100",
Port = 11112,
Enabled = true,
MaxConnections = 10,
TimeoutSeconds = 30,
Priority = 1
};
// Add to configuration
Configuration.Instance.Servers.Add(server);
using DicomProxyCore.Models;
// Study-level query
var studyQuery = new DicomQuery
{
Level = QueryLevel.Study,
StudyInstanceUID = "1.2.3.4.5.6.7.8.9",
PatientName = "DOE^JOHN",
StudyDate = "20240315",
Modality = "CT"
};
// Series-level query
var seriesQuery = new DicomQuery
{
Level = QueryLevel.Series,
StudyInstanceUID = "1.2.3.4.5.6.7.8.9",
SeriesInstanceUID = "1.2.3.4.5.6.7.8.9.10",
Modality = "CT"
};
using DicomProxyCore.Services;
public class CustomStorageManager : IStorageManager
{
public string GetStudyPath(string studyInstanceUID)
{
return Path.Combine(Configuration.Instance.StoragePath, "studies", studyInstanceUID);
}
public string GetSeriesPath(string studyInstanceUID, string seriesInstanceUID)
{
return Path.Combine(GetStudyPath(studyInstanceUID), seriesInstanceUID);
}
public string GetInstancePath(string studyInstanceUID, string seriesInstanceUID, string sopInstanceUID)
{
return Path.Combine(GetSeriesPath(studyInstanceUID, seriesInstanceUID), $"{sopInstanceUID}.dcm");
}
public async Task<bool> StoreInstanceAsync(DicomDataset dataset)
{
var studyUID = dataset.GetSingleValue<string>(DicomTag.StudyInstanceUID);
var seriesUID = dataset.GetSingleValue<string>(DicomTag.SeriesInstanceUID);
var instanceUID = dataset.GetSingleValue<string>(DicomTag.SOPInstanceUID);
var filePath = GetInstancePath(studyUID, seriesUID, instanceUID);
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
var file = new DicomFile(dataset);
await file.SaveAsync(filePath);
return true;
}
}
using DicomProxyCore.Services;
public class CustomMessaging : IMessaging
{
public event EventHandler<MessageEventArgs> MessageReceived;
public void SendMessage(string message, MessageType type = MessageType.Info)
{
var args = new MessageEventArgs
{
Message = message,
Type = type,
Timestamp = DateTime.Now
};
MessageReceived?.Invoke(this, args);
}
public void Start()
{
// Initialize messaging system
Console.WriteLine("Messaging system started");
}
public void Stop()
{
// Cleanup messaging system
Console.WriteLine("Messaging system stopped");
}
}
using DicomProxyCore.Services;
public class CustomLicenseManager : ILicenseManager
{
public LicenseStatus GetLicenseStatus()
{
return new LicenseStatus
{
IsValid = true,
ExpirationDate = DateTime.Now.AddYears(1),
LicenseType = "Commercial",
Features = new[] { "DICOM", "REST_API", "PROXY" }
};
}
public bool ValidateLicense(string licenseKey)
{
// Custom license validation logic
return !string.IsNullOrEmpty(licenseKey) && licenseKey.Length > 10;
}
}
using DicomProxyCore.Services;
using Microsoft.Extensions.DependencyInjection;
public void ConfigureServices(IServiceCollection services)
{
// Register core services
services.AddSingleton<IStorageManager, CustomStorageManager>();
services.AddSingleton<IMessaging, CustomMessaging>();
services.AddSingleton<ILicenseManager, CustomLicenseManager>();
services.AddSingleton<ICStoreConnectionsManager, CStoreConnectionsManager>();
// Register configuration
services.AddSingleton(Configuration.Instance);
// Register logging
services.AddLogging(builder =>
{
builder.AddNLog("nlog.config");
});
}
using DicomProxyCore.Services;
// Using dependency service (existing pattern)
var messaging = DependencyService.Get<IMessaging>();
var storage = DependencyService.Get<IStorageManager>();
// Using standard DI container
var services = new ServiceCollection();
ConfigureServices(services);
var serviceProvider = services.BuildServiceProvider();
var messaging2 = serviceProvider.GetRequiredService<IMessaging>();
using DicomProxyCore.Dicom;
using FellowOakDicom;
public class DicomProcessor
{
public async Task ProcessDicomFile(string filePath)
{
// Load DICOM file
var dicomFile = await DicomFile.OpenAsync(filePath);
var dataset = dicomFile.Dataset;
// Extract metadata
var studyUID = dataset.GetSingleValue<string>(DicomTag.StudyInstanceUID);
var patientName = dataset.GetSingleValueOrDefault(DicomTag.PatientName, "Unknown");
var modality = dataset.GetSingleValueOrDefault(DicomTag.Modality, "");
// Convert to JSON
var jsonConverter = new DicomJson2();
var jsonString = jsonConverter.WriteToString(dataset);
// Process image data
if (dataset.Contains(DicomTag.PixelData))
{
await ProcessImageData(dataset);
}
}
private async Task ProcessImageData(DicomDataset dataset)
{
// Extract image
var image = new DicomImage(dataset);
// Get image properties
var width = dataset.GetSingleValue<ushort>(DicomTag.Columns);
var height = dataset.GetSingleValue<ushort>(DicomTag.Rows);
var bitsAllocated = dataset.GetSingleValue<ushort>(DicomTag.BitsAllocated);
Console.WriteLine($"Image: {width}x{height}, {bitsAllocated} bits");
// Render to bitmap (requires DicomProxyCore.Imaging.SkiaSharp)
using var bitmap = image.RenderImage().AsClonedBitmap();
// Save as JPEG
await bitmap.SaveAsync("output.jpg");
}
}
using DicomProxyCore.Helper;
using FellowOakDicom;
public class QueryBuilder
{
public DicomDataset BuildStudyQuery(string patientName = null, string studyDate = null, string modality = null)
{
var query = new DicomDataset
{
{ DicomTag.QueryRetrieveLevel, "STUDY" }
};
// Add search criteria
if (!string.IsNullOrEmpty(patientName))
query.AddOrUpdate(DicomTag.PatientName, patientName);
if (!string.IsNullOrEmpty(studyDate))
query.AddOrUpdate(DicomTag.StudyDate, studyDate);
if (!string.IsNullOrEmpty(modality))
query.AddOrUpdate(DicomTag.ModalitiesInStudy, modality);
// Add return attributes
query.AddOrUpdate(DicomTag.StudyInstanceUID, "");
query.AddOrUpdate(DicomTag.StudyDescription, "");
query.AddOrUpdate(DicomTag.StudyDate, "");
query.AddOrUpdate(DicomTag.StudyTime, "");
query.AddOrUpdate(DicomTag.AccessionNumber, "");
query.AddOrUpdate(DicomTag.PatientID, "");
return query;
}
}
using DicomProxyCore.Commands;
public class ImportFolderCommand : ICommand
{
private readonly string _folderPath;
private readonly IStorageManager _storageManager;
public ImportFolderCommand(string folderPath, IStorageManager storageManager)
{
_folderPath = folderPath;
_storageManager = storageManager;
}
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
var dicomFiles = Directory.GetFiles(_folderPath, "*.dcm", SearchOption.AllDirectories);
foreach (var filePath in dicomFiles)
{
if (cancellationToken.IsCancellationRequested)
break;
try
{
var dicomFile = await DicomFile.OpenAsync(filePath);
await _storageManager.StoreInstanceAsync(dicomFile.Dataset);
Console.WriteLine($"Imported: {filePath}");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to import {filePath}: {ex.Message}");
}
}
}
}
// Usage
var command = new ImportFolderCommand("C:\\DICOM\\Import", storageManager);
await command.ExecuteAsync();
using DicomProxyCore.Services.HostedServices;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public class CustomBackgroundService : BackgroundService
{
private readonly ILogger<CustomBackgroundService> _logger;
private readonly IStorageManager _storageManager;
public CustomBackgroundService(
ILogger<CustomBackgroundService> logger,
IStorageManager storageManager)
{
_logger = logger;
_storageManager = storageManager;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// Perform background task
await PerformMaintenanceAsync();
// Wait for next cycle (1 hour)
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in background service");
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
private async Task PerformMaintenanceAsync()
{
_logger.LogInformation("Performing maintenance tasks");
// Cleanup old files
await CleanupOldFilesAsync();
// Update statistics
await UpdateStatisticsAsync();
}
private async Task CleanupOldFilesAsync()
{
// Implementation for cleaning up old files
_logger.LogInformation("Cleanup completed");
}
private async Task UpdateStatisticsAsync()
{
// Implementation for updating statistics
_logger.LogInformation("Statistics updated");
}
}
using DicomProxyCore.Imaging.SkiaSharp;
using FellowOakDicom.Imaging;
using SkiaSharp;
public class ImageProcessor
{
public async Task<byte[]> ConvertToJpegAsync(DicomDataset dataset, int quality = 90)
{
using var dicomImage = new DicomImage(dataset);
using var bitmap = dicomImage.RenderImage().AsClonedBitmap();
using var stream = new MemoryStream();
using var skBitmap = SKBitmap.FromImage(bitmap);
using var skData = skBitmap.Encode(SKEncodedImageFormat.Jpeg, quality);
skData.SaveTo(stream);
return stream.ToArray();
}
public async Task<byte[]> ApplyWindowingAsync(DicomDataset dataset, double center, double width)
{
using var dicomImage = new DicomImage(dataset);
// Apply windowing
var windowedImage = dicomImage.RenderImage(center, width);
using var bitmap = windowedImage.AsClonedBitmap();
using var stream = new MemoryStream();
using var skBitmap = SKBitmap.FromImage(bitmap);
using var skData = skBitmap.Encode(SKEncodedImageFormat.Png, 100);
skData.SaveTo(stream);
return stream.ToArray();
}
public async Task<byte[]> ResizeImageAsync(DicomDataset dataset, int width, int height)
{
using var dicomImage = new DicomImage(dataset);
using var originalBitmap = dicomImage.RenderImage().AsClonedBitmap();
using var skBitmap = SKBitmap.FromImage(originalBitmap);
using var resizedBitmap = skBitmap.Resize(new SKImageInfo(width, height), SKFilterQuality.High);
using var resizedImage = SKImage.FromBitmap(resizedBitmap);
using var data = resizedImage.Encode(SKEncodedImageFormat.Jpeg, 90);
using var stream = new MemoryStream();
data.SaveTo(stream);
return stream.ToArray();
}
}
using DicomProxyCore.Scripting;
using Jint;
public class ScriptingService
{
private readonly JSScriptingService _scriptingService;
public ScriptingService()
{
_scriptingService = new JSScriptingService();
}
public async Task<object> ExecuteScriptAsync(string script, DicomDataset dataset)
{
var engine = new Engine();
// Add DICOM dataset to script context
engine.SetValue("dataset", dataset);
engine.SetValue("console", new { log = new Action<object>(Console.WriteLine) });
// Add helper functions
engine.SetValue("getTag", new Func<string, string>(tagName =>
{
var tag = DicomTag.Parse(tagName);
return dataset.GetSingleValueOrDefault(tag, "");
}));
// Execute script
var result = engine.Evaluate(script);
return result.ToObject();
}
public async Task<bool> ValidateDataAsync(DicomDataset dataset)
{
var script = @"
var patientName = getTag('00100010');
var studyUID = getTag('0020000D');
var modality = getTag('00080060');
if (!patientName || patientName.trim() === '') {
console.log('Missing patient name');
return false;
}
if (!studyUID || studyUID.trim() === '') {
console.log('Missing study instance UID');
return false;
}
return true;
";
var result = await ExecuteScriptAsync(script, dataset);
return Convert.ToBoolean(result);
}
}
using DicomProxyCore.Database;
using SQLite;
public class DatabaseService
{
private readonly SQLiteAsyncConnection _database;
public DatabaseService(string dbPath)
{
_database = new SQLiteAsyncConnection(dbPath);
InitializeDatabaseAsync().Wait();
}
private async Task InitializeDatabaseAsync()
{
await _database.CreateTableAsync<StudyRecord>();
await _database.CreateTableAsync<SeriesRecord>();
await _database.CreateTableAsync<InstanceRecord>();
}
public async Task<int> InsertStudyAsync(StudyRecord study)
{
return await _database.InsertAsync(study);
}
public async Task<List<StudyRecord>> GetStudiesAsync(string patientName = null, string studyDate = null)
{
var query = _database.Table<StudyRecord>();
if (!string.IsNullOrEmpty(patientName))
query = query.Where(s => s.PatientName.Contains(patientName));
if (!string.IsNullOrEmpty(studyDate))
query = query.Where(s => s.StudyDate == studyDate);
return await query.ToListAsync();
}
public async Task DeleteStudyAsync(string studyInstanceUID)
{
await _database.DeleteAsync<StudyRecord>(studyInstanceUID);
await _database.Table<SeriesRecord>()
.Where(s => s.StudyInstanceUID == studyInstanceUID)
.DeleteAsync();
await _database.Table<InstanceRecord>()
.Where(i => i.StudyInstanceUID == studyInstanceUID)
.DeleteAsync();
}
}
[Table("Studies")]
public class StudyRecord
{
[PrimaryKey]
public string StudyInstanceUID { get; set; }
public string PatientName { get; set; }
public string PatientID { get; set; }
public string StudyDate { get; set; }
public string StudyTime { get; set; }
public string StudyDescription { get; set; }
public string AccessionNumber { get; set; }
public string Modality { get; set; }
public DateTime CreatedDate { get; set; }
}
using NLog;
using System.Diagnostics;
public class LoggingService
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
public static void LogDicomOperation(string operation, string aeTitle, TimeSpan duration, bool success)
{
logger.Info("DICOM {Operation} from {AETitle} took {Duration}ms - {Status}",
operation, aeTitle, duration.TotalMilliseconds, success ? "Success" : "Failed");
}
public static void LogApiRequest(string method, string path, int statusCode, TimeSpan duration)
{
logger.Info("API {Method} {Path} returned {StatusCode} in {Duration}ms",
method, path, statusCode, duration.TotalMilliseconds);
}
public static void LogError(Exception ex, string context)
{
logger.Error(ex, "Error in {Context}: {Message}", context, ex.Message);
}
}
// Usage in your code
public async Task<DicomCStoreResponse> OnCStoreRequest(DicomCStoreRequest request)
{
var stopwatch = Stopwatch.StartNew();
var success = false;
try
{
// Process the request
await ProcessStoreRequest(request);
success = true;
return new DicomCStoreResponse(request, DicomStatus.Success);
}
catch (Exception ex)
{
LoggingService.LogError(ex, "C-STORE Processing");
return new DicomCStoreResponse(request, DicomStatus.ProcessingFailure);
}
finally
{
stopwatch.Stop();
LoggingService.LogDicomOperation("C-STORE", request.CalledAE, stopwatch.Elapsed, success);
}
}
using Xunit;
using DicomProxyCore;
using DicomProxyCore.Services;
using Microsoft.Extensions.DependencyInjection;
public class DicomProxyTests
{
private readonly ServiceProvider _serviceProvider;
public DicomProxyTests()
{
var services = new ServiceCollection();
services.AddSingleton<IStorageManager, TestStorageManager>();
services.AddSingleton<IMessaging, TestMessaging>();
_serviceProvider = services.BuildServiceProvider();
}
[Fact]
public async Task TestDicomStore()
{
// Arrange
var storageManager = _serviceProvider.GetService<IStorageManager>();
var dataset = new DicomDataset
{
{ DicomTag.PatientName, "TEST^PATIENT" },
{ DicomTag.StudyInstanceUID, "1.2.3.4.5" },
{ DicomTag.SeriesInstanceUID, "1.2.3.4.5.6" },
{ DicomTag.SOPInstanceUID, "1.2.3.4.5.6.7" }
};
// Act
var result = await storageManager.StoreInstanceAsync(dataset);
// Assert
Assert.True(result);
}
[Fact]
public void TestConfigurationSetup()
{
// Arrange
var basePath = Path.GetTempPath();
// Act
Configuration.Setup(basePath, false);
// Assert
Assert.NotNull(Configuration.Instance);
Assert.Equal(basePath, Configuration.Instance.ConfigRoot);
}
}
public class TestStorageManager : IStorageManager
{
public async Task<bool> StoreInstanceAsync(DicomDataset dataset)
{
// Mock implementation for testing
return await Task.FromResult(true);
}
public string GetStudyPath(string studyInstanceUID) => "/test/path";
public string GetSeriesPath(string studyInstanceUID, string seriesInstanceUID) => "/test/path";
public string GetInstancePath(string studyInstanceUID, string seriesInstanceUID, string sopInstanceUID) => "/test/path";
}
using DicomProxyCore.CStoreSCP.ConnectionManager;
using System.Collections.Concurrent;
public class OptimizedConnectionManager
{
private readonly ConcurrentDictionary<string, DicomClient> _connectionPool;
private readonly SemaphoreSlim _poolSemaphore;
private readonly int _maxConnections;
public OptimizedConnectionManager(int maxConnections = 50)
{
_maxConnections = maxConnections;
_connectionPool = new ConcurrentDictionary<string, DicomClient>();
_poolSemaphore = new SemaphoreSlim(maxConnections, maxConnections);
}
public async Task<DicomClient> GetConnectionAsync(Connection server)
{
await _poolSemaphore.WaitAsync();
var key = $"{server.Hostname}:{server.Port}:{server.AE_Title}";
if (_connectionPool.TryGetValue(key, out var existingClient) &&
existingClient.IsConnected)
{
return existingClient;
}
var client = DicomClientFactory.Create(server.Hostname, server.Port, false, server.AE_Title, server.AE_Title);
client.Timeout = TimeSpan.FromSeconds(server.TimeoutSeconds);
_connectionPool.AddOrUpdate(key, client, (k, v) => client);
return client;
}
public void ReleaseConnection(DicomClient client)
{
_poolSemaphore.Release();
}
}
using LazyCache;
public class CachingService
{
private readonly IAppCache _cache;
public CachingService(IAppCache cache)
{
_cache = cache;
}
public async Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiry = null)
{
return await _cache.GetOrAddAsync(key, factory, expiry ?? TimeSpan.FromHours(1));
}
public async Task<List<StudyRecord>> GetStudiesWithCaching(string patientName)
{
var cacheKey = $"studies:{patientName}";
return await GetOrAddAsync(cacheKey, async () =>
{
// Expensive database operation
return await DatabaseService.GetStudiesAsync(patientName);
}, TimeSpan.FromMinutes(15));
}
}
<!-- SDK.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<PackageId>MyCompany.DicomProxy.SDK</PackageId>
<Version>1.0.0</Version>
<Authors>My Company</Authors>
<Description>SDK for DICOM Proxy integration</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DicomProxyCore" Version="1.09.1" />
</ItemGroup>
</Project>
# Dockerfile for SDK-based application
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8080
EXPOSE 11112
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "."]
RUN dotnet restore "./MyApp.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "MyApp.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
This SDK documentation provides comprehensive coverage of the DicomProxyCore SDK components and integration patterns. For specific implementation examples, refer to the Examples & Tutorials section.