Import and export¶
Users bring rows in from CSV, Excel or JSON files and take them out again, without you writing a parser or an upload page. Every imported row runs through your own command, so permissions, the organisation, validation and the audit log apply exactly as when someone fills in the form.
Opt in per entity set¶
An entity set can be imported once you register how a row becomes a command. Sets without a registration stay export-only.
// create and update: the id is null for new rows and the found row's id for updates
services.AddODataImport("Brands",
(Guid? id, AddEditBrandRequest row) => new AddEditBrandCommand(id, row),
id => new DeleteBrandsCommand([id])); // optional: makes the last import undoable
// create only
services.AddODataImport("Notes", (AddNoteRequest row) => new AddNoteCommand(row));
services.AddODataImport<AddNoteCommand>("Notes"); // the command itself is the row
The row type's public properties with simple types (text, numbers, dates, Guid, bool, enums) are the fields a column can be mapped to. A field's label comes from [Display], [DisplayName] or [Description], otherwise from its name ("MadeAt" reads "Made at"). [Required] and required mark fields the wizard asks for.
A Guid property such as BrandId next to a navigation Brand of another OData set is a reference: the file may contain the brand's name instead of its id. The name comes from the first of Name, Title, DisplayName, Code and Key, otherwise the first text property.
The wizard¶
CoworkeeDataTable shows an Import button with Importable="true". It opens a side sheet in four steps:
- File: CSV, TSV, Excel (
.xlsx) or JSON up to 20 MB. For text files the wizard detects encoding (byte order mark, otherwise UTF-8 or Windows-1252), separator and whether the first row is a header, and you can correct each of them. - Columns: every column gets a suggested field, matched by name, label or the label in the user's language. You choose add new rows, update existing rows or update or add; the last two need a key column, for example the id or the name. For text files you set the language of numbers and dates (
1.234,50and15.01.2026inde-DE). A mapping can be saved under a name per entity set and loaded again for the next file of the same supplier. - Check: the first 200 rows run through conversion, key lookup and the FluentValidation validators of the commands, without writing anything. You see what each row would do and why a row fails.
- Import: a dry run checks every row in the background, then Import writes them. Both run as a background job; the progress arrives live over the user's realtime topic. Failed rows can be downloaded as CSV with an extra Error column, fixed and imported again. Undo import deletes the rows the user's last import of that set created (updated rows keep their new values).
Updates start from the stored row: columns you did not map keep their values.
Values are read like this:
| Type | Accepted |
|---|---|
| Numbers | in the chosen language for text files, invariant for Excel and JSON; whole numbers refuse fractions |
| Dates | ISO first (2026-01-15), then the chosen language |
| Booleans | true/false, yes/no, y/n, 1/0, x, on/off and yes/no in the user's language |
| Enums | name, number, label ([Display]/[Description]) or its translation |
| References | id or display text, only rows the user may read |
Admin page¶
Administration → System → Import and export (/admin/data-transfer, permission DataTransfer.Use) lists every set the user may import, exports whole sets and shows the user's imports and exports with their result files and the undo.
Export¶
The export menu of a table writes the current view: search, facets, column filters, sort and the visible columns. Excel, CSV and JSON come from the server:
Up to Coworkee:OData:BackgroundExportRows rows (default 10 000) the file comes right back. Larger exports answer 202 Accepted with a job; the user gets a notice with a Download button when the file is ready. Coworkee:OData:MaxExportRows (default 100 000) caps every export. Permissions, entity filters and hidden properties apply as in the list.
Progress stream and one-time download¶
GET /api/v1/data/{set}/export/stream takes the same query and answers application/x-ndjson, one DataExportProgress per line: counting, reading (0-80 %, every 500 rows), writing, then ready with url (api/v1/data/downloads/{token}, relative to the API), fileName and jobId, or failed with a message. The link needs no sign-in, so a native <a download> works; it opens the file once and only for five minutes (IAccessTokens, see files). The export runs without the request's cancellation: when the client leaves before ready, it is written all the same and mailed to the user. Either way the user's job list keeps the file. IDataTransferApi.StreamExportAsync reads the lines in the Blazor client.
More rows than BackgroundExportRows end the stream with queued and the jobId: the export job writes the file, as for any background export (and mails it when the client had left). A user has at most Coworkee:OData:MaxConcurrentExports (default 2) exports running or queued; another stream, background export (GET .../export above BackgroundExportRows) or mailed export answers 429. The check and the new job happen under a lock per user, so parallel requests cannot pass it together. A streamed export is cancelled after Coworkee:OData:ExportTimeout (default 30 minutes) and its job fails; exports running longer than that plus five minutes since they started (StartedAt; a server stopped while writing them) and exports queued for a day without starting fail: the job coworkee-stale-exports sweeps them every five minutes, and a user's own ones also when he starts the next export, so none stays running for ever. Time in the queue does not count. An export job runs only a job that is still queued, and a job the sweep failed meanwhile stays failed (Status is a concurrency token).
Mailed and scheduled exports¶
POST /api/v1/data/{set}/export/mail with MailExportRequest(Query, Recipients, FileName) writes the export as a background job and mails it to the user and the recipients (202 with the job). Recipients other than the user need DataTransfer.ExportSchedules.
Scheduled exports (DataTransfer.ExportSchedules plus the set's own permission) mail a list on a schedule: ExportSchedule (table cw.ExportSchedules) keeps the set, the query string verbatim ($filter, $search, $orderby, columns, format), a name, a file name, further recipients, the selected groups and a ReportSchedule: daily, weekly on a weekday or monthly on a day or on the first to fourth or last weekday (Week), every n-th period (Every), at a time of a time zone. The job coworkee-export-schedules (every five minutes) runs each due schedule as its owner, so the owner's permissions, row filters and groups decide what the file holds; each run is a job of the owner with the file, and errors stay on the schedule (LastError). A run sends nothing once the owner is deactivated or has lost DataTransfer.ExportSchedules or the set's permission; the reason is the schedule's LastError. A schedule or background export saved with selected groups lists only those the owner still has access to; when none is left it fails instead of listing every group.
| Call | Purpose |
|---|---|
GET /api/v1/data/export-schedules?entitySet= |
the user's schedules |
POST /api/v1/data/export-schedules, PUT .../{id} |
create or change one (SaveExportScheduleRequest); the query passes the set's query guard at once (readable properties, navigations, no parameter aliases): 403 or 400 |
DELETE /api/v1/data/export-schedules/{id} |
delete one |
POST /api/v1/data/export-schedules/{id}/run |
send it now; the schedule stays |
The export menu of a table offers Schedule export to holders of the permission; the page Scheduled exports (/data/export-schedules, key export-schedules) lists them with Send now, edit and delete. The mail is the template Data.Export (Mailing module, 23 languages); files above 10 MB are not attached, the mail then points to the app. Without the Mailing module nothing is mailed (IExportMailer). App values such as a currency stay in the query string; the app reads them itself.
HTTP API¶
| Call | Purpose |
|---|---|
GET /api/v1/data/imports |
importable sets with their fields |
POST /api/v1/data/{set}/import/file |
upload (multipart, field file), answers the detected columns |
POST /api/v1/data/imports/{id}/sheet |
read the upload again with another separator, encoding or header |
POST /api/v1/data/imports/{id}/preview |
check the first rows with ImportSettings |
POST /api/v1/data/imports/{id}/start |
run the import or a dry run (dryRun: true) as a job |
POST /api/v1/data/imports/{id}/undo |
undo the last import |
GET/POST /api/v1/data/{set}/import/profiles, DELETE /api/v1/data/import-profiles/{id} |
saved mappings |
GET /api/v1/data/jobs, GET /api/v1/data/jobs/{id}, GET /api/v1/data/jobs/{id}/file |
the user's jobs and their files |
GET /api/v1/data/{set}/export/stream, GET /api/v1/data/downloads/{token} |
export with progress lines, its one-time download (no sign-in) |
POST /api/v1/data/{set}/export/mail |
export as job, mailed |
POST /api/v1/data/{set}/import |
one-step import of earlier versions: columns named like the fields, new rows only |
Status: uploads and result files are kept in the job row in the database, which suits files of a few MB. Key and reference lookups run one query per row and new text. Undo deletes created rows only.