Skip to content

DHI.Services.Documents.WebApi — Internal Developer Guide

Expose document upload/download/search over HTTP using the Documents Web API package. This guide tracks the existing controller behavior precisely.

Source sample (ready to run): GitHub – Documents Host https://github.com/DHI/DomainServicesSamples/tree/main/Host/Documents

What you get

  • Route base: api/documents/{connectionId} (API v1)
  • Controller: DocumentsController
    • Upload (POST …/{id})
    • Download (GET …/{id})
    • Delete (DELETE …/{id})
    • Count / Ids (GET …/count, GET …/ids)
    • Metadata (GET …/{id}/metadata, GET …/metadata?filter=…)
    • Grouped listing (GET …/fullnames, GET …?group=…) — only for grouped connections

Services are resolved from ServiceLocator by {connectionId}. See Connections below.

Endpoints (quick matrix)

Base: /api/documents/{connectionId}

Method Path Query/body Auth Response Notes
GET /{id} [Authorize] File stream Content type mapping is limited (see §5). IDs with slashes use | encoding (see §3). Missing docs map to 404 via exception middleware (see §4).
POST /{id} multipart/form-data with single file field; any query params → metadata; controller adds fileName EditorsOnly 201 Created If {id} has no extension, the uploaded file’s extension is appended before storing. Validators run automatically (see §6).
DELETE /{id} AdministratorsOnly 204 No Content 404 via exception mapping if not found.
GET /count [Authorize] int Total documents.
GET /ids [Authorize] string[] Flat IDs (full names).
GET /{id}/metadata [Authorize] Dictionary<string,string> 404 via exception mapping if not found.
GET /metadata filter=... (optional) + any extra query params forwarded to repo [Authorize] Dictionary<string, Dictionary<string,string>> When filter omitted, returns all metadata. (Schema note in §7.)
GET /fullnames group=... (optional) [Authorize] string[] Grouped connections only.
GET / group=... (optional) [Authorize] Document<string>[] Grouped connections only. (Schema note in §7.)

IDs & groups in URLs

The controller uses FullNameString.FromUrl(...) to decode IDs/groups:

  • Encode slashes / as | in URLs: Example: reports/2024/summary.pdfreports|2024|summary.pdf
  • Escape a literal | with ||.

Missing documents & 404s

  • DocumentService.Get(id) throws KeyNotFoundException when missing (it checks Contains first).
  • The controller relies on exception middleware to turn that into a 404 JSON error.
  • The if (stream is null) return NotFound() branch in the controller rarely triggers in practice.

Doc truth: clients should expect 404 for missing resources, delivered via the centralized exception mapping.

Content types

The controller maps a small allow-list of extensions to content types:

  • Images: bmp|gif|jpeg|png|tiffimage/{ext}
  • zip|pdfapplication/{ext}
  • csv|html|xmltext/{ext}
  • Otherwise → application/octet-stream

MIKECloud note: MIKECloud repos return fileType with a leading dot (e.g., .pdf). Because the controller compares without the dot, many MIKECloud downloads will fall back to application/octet-stream. Clients should rely on the Content-Disposition filename (and/or their own MIME detection) if they need a precise type.

Validators (HTTP uploads)

  • POST collects all query parameters into Parameters and adds "fileName" = uploadedFile.FileName.
  • Because "fileName" is present, any registered validators whose regex matches will run.
  • On validator failure, the controller returns 400 Bad Request with the validator’s message (via thrown ArgumentException).

Response shapes & Swagger note

Two actions return richer shapes than their method signatures imply:

  • GET /metadata?filter=... — returns a map of ID → metadata map Actual shape: Dictionary<string, Dictionary<string,string>>
  • GET / (grouped listing) — returns a list of documents Actual shape: Document<string>[]

Swagger may display simplified schemas. The payloads above are the authoritative shapes.

Minimal wiring

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 1) Resolve [AppData] for your process (optional but convenient)
AppDomain.CurrentDomain.SetData("DataDirectory",
    Path.Combine(builder.Environment.ContentRootPath, "App_Data"));

// 2) MVC + versioning + Swagger (optional)
builder.Services.AddControllers()
    .AddJsonOptions(o =>
    {
        foreach (var c in SerializerOptionsDefault.Options.Converters)
            o.JsonSerializerOptions.Converters.Add(c);
    });
builder.Services.AddApiVersioning();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// 3) AuthN/AuthZ (sample)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdministratorsOnly",
        p => p.RequireClaim(System.Security.Claims.ClaimTypes.GroupSid, "Administrators"));
    options.AddPolicy("EditorsOnly",
        p => p.RequireClaim(System.Security.Claims.ClaimTypes.GroupSid, "Editors"));
});

// 4) (Optional) for admin UIs that discover providers
builder.Services.AddScoped(_ => new ConnectionTypeService(AppContext.BaseDirectory));

var app = builder.Build();

// 5) DHI exception mapping middleware
app.UseExceptionHandlingWithLogging();

app.UseSwagger(); app.UseSwaggerUI();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

// 6) Register at least one document service
var root = Path.Combine(builder.Environment.ContentRootPath, "App_Data", "docs");
Directory.CreateDirectory(root);
ServiceLocator.Register(
    new GroupedDocumentService<string>(new FileDocumentRepository(root)),
    "files" // your {connectionId}
);

app.Run();

See Hands-on sample host here: https://github.com/DHI/DomainServicesSamples/tree/main/Host/Documents/DHI.Services.Documents.WebApi.Host

Connections (configurable providers)

At startup you can register services directly (as above) or via connections objects:

  • DocumentServiceConnection / GroupedDocumentServiceConnection (WebApi variants) resolve [AppData] and [env:VAR] in the connection string before creating the repo.

Example (file repo, grouped):

{
  "$type": "DHI.Services.Documents.WebApi.GroupedDocumentServiceConnection, DHI.Services.Documents.WebApi",
  "RepositoryType": "DHI.Services.Documents.FileDocumentRepository, DHI.Services.Documents",
  "ConnectionString": "[AppData]docs",
  "Name": "Local docs (grouped)",
  "Id": "files"
}

Security model (defaults)

  • GET/listing/metadata → [Authorize] (authenticated users)
  • POST …/{id}EditorsOnly
  • DELETE …/{id}AdministratorsOnly

Adjust policies/claims to your environment.

Provider-specific notes

  • MIKECloud (grouped)
    • Get returns a rewound stream; fileType includes leading dot (e.g., .csv).
    • GetAll() derives Group/Name from the dataset name/path surfaced by the platform. If your tenant returns leaf names only, prefer /ids + per-ID metadata for exact grouping.
  • MIKECloud (flat)
    • Get does not rewind; callers should Seek(0, SeekOrigin.Begin) as needed.
    • GetAll, GetAllMetadata, GetMetadataByFilter are intentionally not implemented (and not required by this controller for flat connections).
  • MCLite — see provider guide for XML metadata filtering and group tree options.

Common pitfalls

  • Content type is application/octet-stream for some providers: see §5 (leading dot).
  • “Not found” behavior comes from exception mapping middleware (see §4).
  • IDs containing / must be encoded with | (group|sub|file.ext), and | itself escapes as ||.
  • Validators not running: ensure fileName is present in parameters (the controller adds it for uploads; your custom callers must, too).