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
- Upload (
Services are resolved from
ServiceLocatorby{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.pdf→reports|2024|summary.pdf - Escape a literal
|with||.
Missing documents & 404s¶
DocumentService.Get(id)throwsKeyNotFoundExceptionwhen missing (it checksContainsfirst).- 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|tiff→image/{ext} zip|pdf→application/{ext}csv|html|xml→text/{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)¶
POSTcollects all query parameters intoParametersand 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}→ EditorsOnlyDELETE …/{id}→ AdministratorsOnly
Adjust policies/claims to your environment.
Provider-specific notes¶
- MIKECloud (grouped)
Getreturns a rewound stream;fileTypeincludes leading dot (e.g.,.csv).GetAll()derivesGroup/Namefrom 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)
Getdoes not rewind; callers shouldSeek(0, SeekOrigin.Begin)as needed.GetAll,GetAllMetadata,GetMetadataByFilterare 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-streamfor 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
fileNameis present in parameters (the controller adds it for uploads; your custom callers must, too).