How to use Middleware in DICOM Web Server
In DicomObjects .NET 8 version, users can define their own self-hosted server start up and use the middleware they design.
An example is where the server wish to route custom http web requests (such as baseURL/Prefix/QIDO/..) to the correct QIDO/WADO/STOW controlers.
Sample custom Startup class:
public class DicomWebStartup : IStartup
{
/// <summary>
/// Configure all required services
/// </summary>
/// <param name="services">The IService collection</param>
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddControllersAsServices();
}
/// <summary>
/// Config Application
/// </summary>
/// <param name="app">IApplicationBuild object</param>
public void Configure(IApplicationBuilder app)
{
app.UseCors();
// Appending prefixes for QIDO, WADO and STOW web controlers
app.UseMiddleware<Middleware>("org1", "org1", "org1");
app.UseRouting();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Sample Middleware class:
public class Middleware
{
string[] _qidoBaseTemplates = [
"/Qido/studies/{studyuid}/series/{seriesuid}/instances",
"/Qido/studies/{studyuid}/series",
"/Qido/studies/{studyuid}/instances",
"/Qido/studies",
"/Qido/series",
"/Qido/instances"
];
string[] _wadoBaseTemplates = [
"/studies/{studyuid}/series/{seriesuid}/instances/{instanceuid}/frames/{frames}/rendered", // consumer format (not dicom)
"/studies/{studyuid}/series/{seriesuid}/instances/{instanceuid}/frames/{frames}/", // application/dicom
"/studies/{studyuid}/series/{seriesuid}/instances/{instanceuid}/rendered", // consumer format (not dicom)
"/studies/{studyuid}/series/{seriesuid}/instances/{instanceuid}/metadata", // json or xml
"/studies/{studyuid}/series/{seriesuid}/instances/{instanceuid}/", // application/dicom
"/studies/{studyuid}/series/{seriesuid}/metadata", // json or xml
"/studies/{studyuid}/series/{seriesuid}/rendered", // consumer format (not dicom)
"/{studyuid}/{seriesuid}/{instanceuid}/{*attributes}", // bulk data
"/studies/{studyuid}/series/{seriesuid}/", // application/dicom
"/studies/{studyuid}/rendered", // consumer format (not dicom)
"/studies/{studyuid}/metadata", // json or xml
"/studies/{studyuid}/", // application/dicom
];
string[] _stowBaseTemplates =
[
"/studies",
"/studies/{studyuid}"
];
HashSet<string> qidoTemplates = [];
HashSet<string> wadoTemplates = [];
HashSet<string> stowTemplates = [];
private readonly RequestDelegate _next;
private readonly string _wadoPrefix;
private readonly string _qidoPrefix;
private readonly string _stowPrefix;
public Middleware(RequestDelegate next, string wadoPrefix = "", string qidoPrefix = "", string stowPrefix = "")
{
_next = next;
_wadoPrefix = wadoPrefix;
_qidoPrefix = qidoPrefix;
_stowPrefix = stowPrefix;
if (_qidoPrefix != "")
{
foreach (var template in _qidoBaseTemplates)
{
qidoTemplates.Add(_qidoPrefix + template);
}
}
if (_wadoPrefix != "")
{
foreach (var template in _wadoBaseTemplates)
{
wadoTemplates.Add(_wadoPrefix + template);
}
}
if (_stowPrefix != "")
{
foreach (var template in _stowBaseTemplates)
{
stowTemplates.Add(_stowPrefix + template);
}
}
}
/**
* Captures every request to the server
*/
public async Task InvokeAsync(HttpContext context)
{
var requestPath = context.Request.Path.Value; // base path to match against query templates
if (!string.IsNullOrEmpty(_qidoPrefix) && requestPath.StartsWith($"/{_qidoPrefix}"))
{
if (IsQido(requestPath, context))
{
context.Request.Path = requestPath.Replace($"/{_qidoPrefix}", "");
context.Request.Headers.Append("org", requestPath.Split("/")[1]);
await _next(context);
return;
}
}
if (!string.IsNullOrEmpty(_wadoPrefix) && requestPath.StartsWith($"/{_wadoPrefix}"))
{
if (IsWado(requestPath, context))
{
context.Request.Path = requestPath.Replace($"/{_wadoPrefix}", "/wado");
await _next(context);
return;
}
}
if (!string.IsNullOrEmpty(_stowPrefix) && requestPath.StartsWith($"/{_stowPrefix}"))
{
if (IsStow(requestPath, context))
{
context.Request.Path = requestPath.Replace($"/{_stowPrefix}", "/stow");
await _next(context);
return;
}
}
// add other controller (worklist, npi, mpps, procedure steps etc. etc.) prefix if needed
return;
}
/**
* Determine if a request should be handled by the qido controller based on
* route and accept headers
*
* Tweak to use case
*/
private bool IsQido(string requestPath, HttpContext context)
{
if (IsHelper(qidoTemplates, requestPath))
{
var accept = context.Request.Headers["Accept"];
foreach (var header in accept)
{
if (header.Contains("application/json") || header.Contains("application/dicom+json"))
{
return true;
}
}
}
return false;
}
/**
* Determine if a request should be handled by the wado controller based on
* route and accept headers
*
* Tweak to use case
*/
private bool IsWado(string requestPath, HttpContext context)
{
if (IsHelper(wadoTemplates, requestPath))
{
if (requestPath.EndsWith("/metadata") || requestPath.EndsWith("/rendered") || requestPath.Contains("/frames/"))
{
return true;
}
var accept = context.Request.Headers["Accept"];
foreach (var header in accept)
{
if (header.Contains("application/dicom"))
{
return true;
}
}
}
return false;
}
/**
* Determine if a request should be handled by the stow controller based on
* route
*
* Tweak to use case
*/
private bool IsStow(string requestPath, HttpContext context)
{
return IsHelper(stowTemplates, requestPath);
}
/**
* Perform literal template matching of request path against templates
*/
private bool IsHelper(HashSet<string> templates, string requestPath)
{
foreach (string routeTemplate in templates)
{
var template = TemplateParser.Parse(routeTemplate);
var matcher = new TemplateMatcher(template, GetDefaults(template));
var values = matcher.TryMatch(requestPath, GetDefaults(template));
if (values) return true;
}
return false;
}
/**
* Retrieve query parameters
*/
private RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate)
{
var result = new RouteValueDictionary();
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.DefaultValue != null)
{
result.Add(parameter.Name, parameter.DefaultValue);
}
}
return result;
}
}
Sample user code:
// Serer Side
const string serverURL = "http://localhost:9999/org1";
DicomWebServer server = new();
Action<ILoggingBuilder> loggingProvider = loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddConsole();
loggingBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Error);
};
// use custom web server start up in the Listen method
_ = server.Listen(serverURL, "", "", false, new DicomWebStartup(), loggingProvider);
_ = server.Listen(asyncServerURL, "", "", true, new DicomWebStartup(), loggingProvider);
// set up DicomWebServer's event handler for QIDO, WADO, STOW etc. etc.
server.QidoReceived += Server_QidoReceived;
server.StowReceived += Server_StowReceived;
server.WadoReceived += Server_WadoReceived;
// Client Side
const string serverURL = "http://localhost:9999/org1";
var dwc = new QidoWebClient($"{serverURL}/Qido")
{
ServerCertificateCustomValidationCallback = ValidateWindowsCertificate
};
DicomDataSet queryDataSet = q.QueryDataSet(); // set up QIDO search parameters
var result = dwc.Query(queryDataSet);