DHI.Services.JsonDocuments.WebApi — Internal Developer Guide¶
This package exposes a small REST API for working with JsonDocument<string> (from DHI.Services.JsonDocuments) and pushes change notifications over SignalR. It’s meant to be hosted in your service and wired to your repository/service registration, or the providers that you choose.
What this package gives you¶
- A controller (
JsonDocumentsController) with CRUD-style endpoints, query & time-window reads, and discovery helpers. - A DTO (
JsonDocumentDTO) for wire-format serialization (including metadata & permissions). - A couple of mapping extensions (
ToDTO,ToDTOs). - SignalR integration: emits group-scoped notifications on add/update/delete.
You bring:
- A
JsonDocumentService<string>bound to your storage provider, registered by connection id. - JWT auth config, logging, filter repository, and SignalR hosting (see Setup).
Setup (Startup.cs)¶
Minimum wiring (replace comments with your own types/paths):
public void ConfigureServices(IServiceCollection services)
{
// AuthN (JWT)
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Audience"],
IssuerSigningKey = RSA.BuildSigningKey(Configuration["Tokens:PublicRSAKey"].Resolve())
};
});
// AuthZ (optional policies)
services.AddAuthorization(options =>
{
options.AddPolicy("AdministratorsOnly",
policy => policy.RequireClaim(ClaimTypes.GroupSid, "Administrators"));
options.AddPolicy("EditorsOnly",
policy => policy.RequireClaim(ClaimTypes.GroupSid, "Editors"));
});
// API versioning + MVC
services.AddApiVersioning(o =>
{
o.ReportApiVersions = true;
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
o.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("api-version", "version", "ver"),
new HeaderApiVersionReader("api-version"));
});
services
.AddCors()
.AddResponseCompression()
.AddControllers()
.AddJsonOptions(o =>
{
o.JsonSerializerOptions.PropertyNamingPolicy = null; // keep names
o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
o.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
// HSTS + Swagger (optional but recommended)
services.AddHsts(o =>
{
o.Preload = true;
o.MaxAge = TimeSpan.FromDays(Configuration.GetValue<double>("AppConfiguration:HstsMaxAgeInDays"));
});
services.AddSwaggerGen(s =>
{
s.SwaggerDoc(Configuration["Swagger:SpecificationName"], new OpenApiInfo {
Title = Configuration["Swagger:DocumentTitle"],
Version = "1",
Description = File.ReadAllText(Configuration["Swagger:DocumentDescription"].Resolve())
});
s.EnableAnnotations();
s.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "DHI.Services.JsonDocuments.WebApi.xml"));
s.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
Description = "Enter the word 'Bearer' followed by a space and the JWT.",
Name = "Authorization", In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey, Scheme = "Bearer"
});
s.AddSecurityRequirement(new OpenApiSecurityRequirement {
{ new OpenApiSecurityScheme {
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" },
Scheme = "oauth2", Name = "Bearer", In = ParameterLocation.Header
}, new List<string>() }
});
});
// REQUIRED Domain services for this API:
services.AddScoped<ILogger>(provider => new SimpleLogger("[AppData]log.json".Resolve()));
services.AddSingleton<IFilterRepository>(provider => new FilterRepository("[AppData]signalr-filters.json".Resolve()));
// SignalR is REQUIRED (notifications)
services.AddSignalR();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); else app.UseHsts();
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseSwagger();
app.UseSwaggerUI(u =>
{
var spec = Configuration["Swagger:SpecificationName"];
u.SwaggerEndpoint($"../swagger/{spec}/swagger.json", Configuration["Swagger:DocumentName"]);
u.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
});
app.UseExceptionHandling();
app.UseResponseCompression();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<NotificationHub>("/notificationhub");
});
// App_Data root for Domain Services
var root = Configuration.GetValue("AppConfiguration:ContentRootPath", env.ContentRootPath);
AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(root, "App_Data"));
// Register your JsonDocumentService<string> by connection id
var service = new JsonDocumentService(
new JsonDocumentRepository() /* // Your provider */
);
ServiceLocator.Register(service, "main"); // <-- connectionId used in routes
}
Important:
- Inject both
ILoggerandIFilterRepositoryas shown.- Enable SignalR and map the
/notificationhub.- Register your
JsonDocumentService<string>in theServiceLocatorunder a connection id (e.g.,"main"). You’ll call the API with/api/jsondocuments/main/....
Routing, versioning, and auth¶
- Base route:
api/jsondocuments/{connectionId} - Version: v1 (query header or query-string, defaults to 1.0)
- Auth:
[Authorize]on the controller — configure JWT as above. - connectionId: selects which service instance to use (
Services.Get<JsonDocumentService<string>>(connectionId)).
DTO model¶
JsonDocumentDTO is the wire contract.
{
"FullName": "configs/simulations/Simulation 42", // required
"Data": "{\"params\":{\"dt\":60}}", // required (stringified JSON)
"DateTime": "2025-01-23T12:34:56Z",
"Added": "2025-01-23T12:35:00Z",
"Updated": "2025-01-24T07:00:00Z",
"Deleted": null,
"Permissions": [ /* PermissionDTO[] (optional) */ ],
"Metadata": { "owner": "ops" } // dictionary, keys serialized as strings
}
Mapping:
- DTO -> Domain:
ToJsonDocument()parsesFullName(group + name), initializesJsonDocument<string>, copiesMetadataandPermissions, and passes through timestamps. - Domain -> DTO: constructor copies fields verbatim.
Endpoints¶
Base: api/jsondocuments/{connectionId}
1) GET / — List or time-window¶
Query:
from(optional, ISO 8601)to(optional, ISO 8601)dataSelectors(optional) — e.g.dataSelectors=[$.params,$.notes]
Behavior:
- If
fromandtoomitted -> all docs. - Else -> docs with
DateTime∈[from, to].
200 ⇒ JsonDocumentDTO[]
2) POST /query — Query body¶
Body: QueryDTO<JsonDocument<string>>
Query: optional dataSelectors.
Returns 200 with JsonDocumentDTO[].
The repository/service controls which query conditions are supported.
3) GET /{fullName} — Get one¶
Path:
fullNameis URL-encoded; useFullNameString.FromUrl(fullName)on the server side.
Query:
- optional
dataSelectors
200 ⇒ JsonDocumentDTO
404 ⇒ not found
4) GET /group/{group} — List by group¶
Path:
groupis URL-encoded; parsed viaFullNameString.FromUrl(group).
Query:
- optional
dataSelectors
200 ⇒ JsonDocumentDTO[]
5) GET /count — Count all¶
200 ⇒ integer
6) GET /fullnames — Discovery¶
Query:
- optional
group(URL-encoded)
200 ⇒ string[] of FullName
7) POST / — Add¶
Body: JsonDocumentDTO (must include FullName and Data).
201 ⇒ JsonDocumentDTO (CreatedAt link targets GET /{fullName})
404 ⇒ (rare) if immediate re-read fails
Side effects:
Emits SignalR "JsonDocumentAdded" to matching groups (see Notifications).
8) PUT / — Update¶
Body: JsonDocumentDTO.
200 ⇒ JsonDocumentDTO
404 ⇒ not found
Side effects:
Emits "JsonDocumentUpdated" to groups.
9) DELETE /{fullName} — Delete¶
Query:
softDelete(bool, defaultfalse)
204 ⇒ hard-deleted 200 ⇒ soft-deleted (returns the soft-deleted entity) 404 ⇒ not found
Side effects:
Emits "JsonDocumentDeleted" to groups.
Data selectors¶
All read endpoints accept dataSelectors to project the JSON. The controller accepts a simple bracketed, comma-separated list:
dataSelectors=[$.params,$.notes]
Parsing is minimal (split by , after trimming [ and ]), then passed to the underlying service which uses JSONPath-like tokens with Argon.JObject.SelectToken.
Tips
- Prefer explicit absolute paths:
$.params.dt,$.notes. - URL-encode the brackets in some clients:
dataSelectors=%5B%24.params,%24.notes%5D - Avoid wildcards/filters (
*,?(…))—the current implementation targets single tokens.
Notifications over SignalR¶
When a document is added, updated, or deleted, the controller discovers relevant groups and sends a message via /notificationhub:
- Hub method (to group):
"JsonDocumentAdded" | "JsonDocumentUpdated" | "JsonDocumentDeleted" - Payload (
Parameters):- Add/Update:
{ id, data, userName } - Delete (hard/soft):
{ id, userName }
- Add/Update:
Who receives messages?¶
Group resolution uses a filter repository (IFilterRepository) via FilterService:
- If a filter has no
QueryConditions-> its group always receives messages. - Else the controller builds
new Query<JsonDocument<string>>(filter.QueryConditions) { new QueryCondition("Id", id) }and uses the service to evaluate it. If any results match, that group receives the message.
Implement/seed your
IFilterRepository(e.g.,FilterRepository("[AppData]signalr-filters.json")) with group filters appropriate for your application.
Request examples¶
Assume you registered your service under connection id main.
Get one¶
curl -H "Authorization: Bearer $JWT" \
'https://host/api/jsondocuments/main/configs%2Fsimulations%2FSimulation%2042?dataSelectors=%5B%24.params,%24.notes%5D'
Add¶
curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{
"FullName": "configs/simulations/Simulation 43",
"Data": "{\"params\":{\"dt\":120},\"notes\":\"candidate\"}",
"DateTime": "2025-01-25T10:00:00Z",
"Metadata": {"owner":"ops"}
}' \
https://host/api/jsondocuments/main
Update¶
curl -X PUT -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{
"FullName": "configs/simulations/Simulation 43",
"Data": "{\"params\":{\"dt\":180},\"notes\":\"retuned\"}"
}' \
https://host/api/jsondocuments/main
Delete (soft)¶
curl -X DELETE -H "Authorization: Bearer $JWT" \
'https://host/api/jsondocuments/main/configs%2Fsimulations%2FSimulation%2043?softDelete=true'
Status codes & errors¶
- 200 OK: successful GET/PUT (PUT returns the updated entity), soft delete returns entity.
- 201 Created: successful POST;
Locationpoints toGET /{fullName}. - 204 No Content: hard delete success.
- 404 Not Found: entity/group missing (depending on endpoint).
- 401/403: auth/authz issues.
Exceptions during SignalR notifications are logged but do not change the API response.
Extensibility & notes¶
- Multiple backends: register multiple
JsonDocumentService<string>instances with different connection ids (e.g.,"main","staging") and call the API with the appropriate/{connectionId}. - DTO evolution: The API defaults to PascalCase JSON; naming policy is disabled (
PropertyNamingPolicy = null). Keep this in mind when introducing clients that assume camelCase. - Metadata keys:
JsonDocumentDTO.MetadatausesDictionary<object, object>in the DTO; when converting to domain it casts keys tostring. - Permissions: DTO maps 1:1 to domain
PermissionviaPermissionDTO. Enforce/inspect in your repository or service if required. - Group & FullName:
FullNameis a combined identifier (group/name). Route parameters for{fullName}and{group}must be URL-encoded; server-side parsing usesFullNameString.FromUrl(...).