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);