A strongly-typed .NET 10 client for the REDCap REST API.
Designed for testability. Typed end-to-end. Faithful to the REDCap wire format.
- Chapter 1 — Introduction
- Chapter 2 — Quick start
- Chapter 3 — Architecture
- Chapter 4 — Design patterns
- Chapter 5 — Common workflows
- Chapter 6 — API reference
- Chapter 7 — Building, testing, contributing
- Chapter 8 — Migration notes
- Acknowledgement of AI assistance
- Glossary
- References
- Appendix A — Directory layout
- Appendix B — Public API method index
- Appendix C — Wire-format enum cheat sheet
- Appendix D — E2E environment variables
- License
REDCap (Research Electronic Data Capture) is a secure, web-based platform for building and managing online surveys and databases, developed and maintained by Vanderbilt University and used at thousands of academic and research institutions worldwide [12]. REDCap exposes its data and project metadata through a form-urlencoded REST API.
redcap-api is a .NET 10 client library that wraps that REST API behind a strongly-typed, async-first C# surface. Rather than asking callers to assemble form payloads by hand, the library exposes one method per REDCap operation — ExportRecordsAsync, ImportMetaDataAsync, ExportDagsAsync, and so on — and uses C# enums (decorated with [Display(Name="…")]) to carry the exact wire values REDCap expects.
- Research data engineers integrating REDCap into a .NET data pipeline.
- Application teams building dashboards, ETL jobs, or admin tooling on top of a REDCap instance.
- Test authors who need to assert REDCap interactions without standing up a real REDCap server.
| Goal | How it shows up in the code |
|---|---|
| Testability | An IRedcapTransport seam lets every test substitute a fake transport; ~95% of tests run without a network. |
| Type safety | C# enums carry wire values through [Display] attributes; misspelling content=record is a compile error. |
| Breadth | Coverage spans every documented REDCap content type — records, metadata, instruments, events, arms, files, file repository, users, user roles, DAGs, surveys, reports, logging, and project info. |
| Async-first | Every public method is async Task<T> with optional CancellationToken and per-call timeout. |
- Library version:
1.0.0 - Target framework:
net10.0 - License: MIT, © 2026 Curtin University.
- .NET 10 SDK
- A REDCap instance and an API token with appropriate rights for the project you intend to access.
using Redcap;
using Redcap.Models;
using var api = new RedcapApi(
"https://your-redcap-instance/api/",
"YOUR_API_TOKEN"
);
// Export records as JSON
var result = await api.ExportRecordsAsync(format: RedcapFormat.json);The token belongs to the client instance — there is no longer a token parameter on every method (see Chapter 8).
| Constructor | When to use it |
|---|---|
new RedcapApi(url, token) |
Most apps. The library creates and disposes its own HttpClient. |
new RedcapApi(url, token, IRedcapTransport transport) |
DI / IHttpClientFactory apps, or tests that want to inject a FakeTransport. The caller owns the transport's lifetime. |
new RedcapApi(url, token, useInsecureCertificates: true) |
Connecting to a development REDCap with a self-signed certificate. Never use in production. |
The library is six layers thick — and the seam in the middle is the whole point.
Figure 3.1 — Layered architecture. The dashed band is the transport seam; everything below it can be swapped at construction time.
- Consumer code holds a
RedcapApiinstance and calls high-level methods likeExportRecordsAsync. RedcapApi(src/RedcapApi/Api/RedcapApi.cs) is a facade — a single class that implements 17 focused domain interfaces (IRedcapRecords,IRedcapMetadata,IRedcapEvents, …) which are aggregated byIRedcap. Consumers can depend on the narrow interface they actually need; the library does not force the whole surface on them.- The payload-building step turns the typed C# call into a
Dictionary<string,string>(orMultipartFormDataContentfor uploads), pulling each enum's wire value from its[Display(Name="…")]attribute viaUtils.GetDisplayName(). This is an adapter between the typed C# world and the form-urlencoded REDCap world. IRedcapTransport(src/RedcapApi/Interfaces/IRedcapTransport.cs) is the abstraction over HTTP. It exposes four operations: form-encoded send returning a stream, form-encoded send returning a string, multipart send, and download-to-disk.DefaultRedcapTransport(src/RedcapApi/Api/DefaultRedcapTransport.cs) is the production implementation. It can own itsHttpClientor borrow one viaDefaultRedcapTransport.FromHttpClient(...).HttpClientdoes the actual network I/O.
Figure 3.2 — Lifecycle of a single ExportRecordsAsync call.
Tracing the steps in Figure 3.2:
- Caller invokes
api.ExportRecordsAsync(format: RedcapFormat.json). RedcapApibuilds a payload dictionary.Content.Record.GetDisplayName()returns"record";RedcapFormat.json.GetDisplayName()returns"json".- The payload is handed to
IRedcapTransport.SendPostRequestAsync. - The transport POSTs
application/x-www-form-urlencodedto the REDCap endpoint. - REDCap returns a JSON body.
- The body bubbles up unchanged to the caller as a
Task<string>, ready forSystem.Text.Jsondeserialization (or, for typed convenience methods, already deserialized).
Patterns are not decoration here — each one solves a concrete problem in a library that has to be tested without a network and extended without breaking the wire format.
IRedcapTransport is the canonical Strategy [3]: one interface, multiple interchangeable implementations selected by the consumer at construction time. Michael Feathers calls the same construct a seam — "a place where you can alter behaviour in your program without editing in that place" [4]. It is the single most important architectural decision in the codebase.
Figure 4.1 — Same RedcapApi instance, two transports.
| File | |
|---|---|
| Interface | src/RedcapApi/Interfaces/IRedcapTransport.cs |
| Production implementation | src/RedcapApi/Api/DefaultRedcapTransport.cs |
| Test fakes | tests/RedcapApi.Tests/RedcapApiTransportTests.cs |
References: GoF Design Patterns — Strategy [3]; Feathers, Working Effectively with Legacy Code — seams [4]; Fowler, Mocks Aren't Stubs [11].
Consumers who just want "give me a REDCap client" can depend on the aggregate IRedcap interface. Internally, IRedcap extends 17 narrower interfaces (one per domain). This is the textbook Facade [3]: one entry point for a subsystem, with the option of using the finer-grained pieces directly.
| File |
|---|
src/RedcapApi/Interfaces/IRedcap.cs |
Reference: GoF Design Patterns — Facade [3].
REDCap is form-urlencoded with lower-case keys (content=record, format=json, action=export). The codebase models each of those parameter spaces as a C# enum, with each member decorated with [Display(Name="…")] [9]. A single extension method, Utils.GetDisplayName(), reads the attribute via System.ComponentModel.DataAnnotations and returns the wire string.
This is an Adapter [3] at the boundary between typed C# and the untyped wire format.
Figure 4.2 — The [Display] name is the wire contract.
| Examples |
|---|
src/RedcapApi/Models/Content.cs, RedcapFormat.cs, RedcapAction.cs, LogType.cs |
References: GoF Design Patterns — Adapter [3]; Microsoft Learn —
DisplayAttribute[9].
Every public method is async Task<T>, accepts an optional CancellationToken, and exposes a timeOutSeconds parameter. There are no synchronous overloads — REDCap is a network resource, and the library is opinionated that callers should treat it that way [8].
Reference: Microsoft Learn — Task-based Asynchronous Pattern (TAP) [8].
The transport seam exists so tests can pin payload shape without a network. FakeTransport, defined inside the test project, implements IRedcapTransport and exposes:
LastDictionaryPayload— the most recent form payload sent.LastMultipartPayload— the most recent multipart payload sent.ResponseBody— a canned response the test author sets up.
Tests then assert on the exact dictionary keys and values the library composed, which is the contract this library is promising to its callers. Following Meszaros's terminology [6], this is a fake — a working implementation that takes a shortcut not suitable for production.
| File |
|---|
tests/RedcapApi.Tests/RedcapApiTransportTests.cs |
References: Meszaros, xUnit Test Patterns — Fake [6]; Fowler, Mocks Aren't Stubs [11].
RedcapApi exposes three constructors, each progressively more flexible. Modern .NET apps register the library through IHttpClientFactory [10] and pass a borrowed HttpClient via the static factory DefaultRedcapTransport.FromHttpClient(...).
services.AddHttpClient("redcap");
services.AddScoped<IRedcap>(sp => {
var http = sp.GetRequiredService<IHttpClientFactory>().CreateClient("redcap");
var transport = DefaultRedcapTransport.FromHttpClient(http, timeOutSeconds: 100);
return new RedcapApi(config["REDCAP_URL"], config["REDCAP_TOKEN"], transport);
});References: Seemann & van Deursen, Dependency Injection Principles, Practices, and Patterns [5]; Microsoft Learn —
IHttpClientFactory[10].
HTTP and REDCap-specific failures are translated to a single RedcapApiException carrying StatusCode and ResponseBody. Callers do not need to catch HTTP exceptions or parse REDCap's error JSON twice. This follows Bloch's guidance to throw exceptions appropriate to the abstraction [7].
| File |
|---|
src/RedcapApi/Exceptions/RedcapApiException.cs |
Reference: Bloch, Effective Java (3rd ed.), Item 73 [7].
var api = new RedcapApi(
"https://your-redcap-instance/api/",
"YOUR_API_TOKEN",
transport
);using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var users = await api.ExportUsersTypedAsync(cancellationToken: cts.Token);If the call does not pass a positive timeOutSeconds, the default transport's own fallback applies:
using Redcap.Api;
using var transport = new DefaultRedcapTransport(timeOutSeconds: 100);
using var api = new RedcapApi("https://your-redcap-instance/api/", "YOUR_API_TOKEN", transport);var httpClient = httpClientFactory.CreateClient("redcap");
var transport = DefaultRedcapTransport.FromHttpClient(httpClient, timeOutSeconds: 100);
var api = new RedcapApi("https://your-redcap-instance/api/", "YOUR_API_TOKEN", transport);To download a file from a REDCap file-upload field, provide a destination directory:
var savedFileName = await api.ExportFileAsync(
record: "1",
field: "consent_pdf",
eventName: "event_1_arm_1",
filePath: "downloads"
);using Redcap.Exceptions;
try
{
var projectInfo = await api.ExportProjectInfoTypedAsync();
}
catch (RedcapApiException ex)
{
Console.WriteLine(ex.StatusCode);
Console.WriteLine(ex.ResponseBody ?? ex.Message);
}Most export operations come in two flavours. The raw form returns a Task<string> containing the REDCap response body, ready for any deserializer the caller prefers. The Typed companion returns deserialized .NET objects — useful when the shape is stable and the caller does not need access to the raw bytes:
// raw
string json = await api.ExportProjectInfoAsync();
// typed
RedcapProjectInfo info = await api.ExportProjectInfoTypedAsync();The library covers every documented REDCap content type. The table below is the canonical at-a-glance map; per-method documentation lives in the DocFX site under docs/index.md.
| Area | Methods |
|---|---|
| Records | ExportRecordsAsync ExportRecordAsync ImportRecordsAsync DeleteRecordsAsync RenameRecordAsync GenerateNextRecordNameAsync |
| Metadata | ExportMetaDataAsync ImportMetaDataAsync ExportFieldNamesAsync |
| Instruments | ExportInstrumentsAsync ExportInstrumentMappingAsync ImportInstrumentMappingAsync ExportPDFInstrumentsAsync |
| Events | ExportEventsAsync ImportEventsAsync DeleteEventsAsync |
| Arms | ExportArmsAsync ImportArmsAsync DeleteArmsAsync |
| Files | ExportFileAsync ImportFileAsync DeleteFileAsync |
| File repository | ExportFilesFoldersFileRepositoryAsync ExportFileFileRepositoryAsync ImportFileRepositoryAsync DeleteFileRepositoryAsync CreateFolderFileRepositoryAsync |
| Users | ExportUsersAsync ImportUsersAsync DeleteUsersAsync |
| User roles | ExportUserRolesAsync ImportUserRolesAsync DeleteUserRolesAsync ExportUserRoleAssignmentAsync ImportUserRoleAssignmentAsync |
| DAGs | ExportDagsAsync ImportDagsAsync DeleteDagsAsync ExportUserDagAssignmentAsync ImportUserDagAssignmentAsync SwitchDagAsync |
| Surveys | ExportSurveyLinkAsync ExportSurveyParticipantsAsync ExportSurveyQueueLinkAsync ExportSurveyReturnCodeAsync ExportSurveyAccessCodeAsync |
| Reports | ExportReportsAsync |
| Logging | ExportLoggingAsync |
| Project | ExportProjectInfoAsync ImportProjectInfoAsync ExportProjectXmlAsync CreateProjectAsync ExportRedcapVersionAsync |
dotnet restore RedcapApi.slnx
dotnet build RedcapApi.slnx -c Release
dotnet test tests/RedcapApi.Tests/RedcapApi.Tests.csproj --verbosity minimal| File | What it covers |
|---|---|
RedcapApiTransportTests.cs |
The bulk of the suite — pins exact payload shapes via FakeTransport. |
ValidationTests.cs |
Argument validation and guards. |
JsonSerializationTests.cs |
System.Text.Json round-trips. |
ConcurrencyTests.cs |
Concurrent use of a shared RedcapApi. |
CancellationTests.cs |
Cancellation propagation. |
HttpErrorTests.cs |
RedcapApiException translation paths. |
InterfaceShapeTests.cs |
Contract / overload-parity assertions. |
LocalHttpServer.cs |
A loopback HttpListener helper for end-to-end transport tests without a REDCap instance. |
RecordsTests.cs |
E2E (tagged [Trait("Category", "E2E")]) — skipped unless Appendix D variables are set. |
Local API documentation lives under docs/index.md and can be previewed with:
dotnet tool restore --configfile NuGet.Config
dotnet restore RedcapApi.slnx --configfile NuGet.Config
dotnet docfx docs/docfx.json --serveSee CONTRIBUTING.md for the endpoint workflow, transport-test expectations, and guidance for adding REDCap API surface area. The house style is: every new endpoint ships with a transport test that pins the payload shape.
// Before
api.ExportRecordsAsync(token, ...);
// Now
var api = new RedcapApi(url, token);
api.ExportRecordsAsync(...);The token now belongs to the client instance rather than being passed to each call. If you inject a custom transport, the caller still owns that transport's lifetime — RedcapApi does not dispose a transport it did not construct.
Parts of this codebase were developed with the assistance of large-language-model tooling — primarily Claude Code (Anthropic) — across the following scopes:
- Refactoring passes — extracting the
IRedcapTransportseam from inlineHttpClientuse, splitting the monolithicIRedcapinto 17 focused domain interfaces, and tightening exception handling. - Test scaffolding — generating the initial transport-test skeletons, parity tests across overloads, and the
LocalHttpServerhelper. - Documentation drafting — DocFX configuration, README revisions (including this one), and code-comment cleanup.
- Design-pattern naming and references — articulating the patterns documented in Chapter 4 and selecting the canonical citations.
The following remained human-driven and reviewed by maintainers before merging:
- The public API surface and naming — what to expose, what to deprecate, and how methods compose.
- REDCap semantic correctness — wire-format decisions,
[Display]names, and behavioural fidelity to the REDCap REST API. - Architectural trade-offs — the choice to use a transport seam, the constructor design, and the typed-vs-raw export split.
- Code review, release decisions, and CHANGELOG curation.
All AI-generated code passed the existing test suite and a maintainer review before being merged. AI is treated here as augmentation, not authorship — consistent with emerging norms for AI use in research-supporting software [1].
| Term | Definition |
|---|---|
| API token | A 32-character secret issued by REDCap that grants a single user access to a single project's API surface. Required for every API call. |
| Arm | A branch of a longitudinal REDCap project that groups events into a logical schedule. |
| async / await | C# keywords for asynchronous, non-blocking I/O. Every public method on RedcapApi is async. |
| CancellationToken | A .NET type used to signal that a long-running operation should stop. Accepted by every RedcapApi method. |
| DAG | Data Access Group — a REDCap mechanism that partitions records by user group. |
| DocFX | Microsoft's static-site generator used here to produce the API documentation site under docs/. |
| Event | A scheduled time point in a longitudinal project (e.g. baseline, 6-month follow-up). |
| Facade | Design pattern: a single entry-point class over a wider subsystem. Embodied here by IRedcap. |
| Field | A named question/variable on a REDCap form (e.g. consent_date). |
| Form / Instrument | A group of related fields presented together to a participant or data-entry user. |
IHttpClientFactory |
The .NET factory that manages HttpClient lifetimes — the recommended way to create HttpClients in DI apps. |
| Longitudinal project | A REDCap project with multiple events/timepoints per record. |
| ODM | Operational Data Model — a CDISC XML format REDCap can export. |
| Project | The top-level REDCap container holding metadata, records, users, and settings. |
| REDCap | Research Electronic Data Capture; a research-data-collection platform from Vanderbilt University. |
| Record | A single row in a REDCap project — typically one participant. |
| Repeating instrument | A form that may be filled in multiple times per record. |
| Seam | A place in code where behaviour can be swapped without editing in place — a Feathers term for the IRedcapTransport interface. |
| Strategy | Design pattern: one interface, multiple interchangeable algorithms. Embodied by IRedcapTransport. |
| Survey | A REDCap instrument exposed as a public web form. |
| TAP | Task-based Asynchronous Pattern — Microsoft's Task<T>-based async convention. |
| Transport | The HTTP-sending component. DefaultRedcapTransport in production, FakeTransport in tests. |
| xUnit | The .NET testing framework used by this project. |
- The REDCap consortium. About REDCap. https://www.project-redcap.org/
- REDCap REST API documentation — typically available at
https://<your-instance>/redcap/api/help/on each REDCap deployment. - Gamma, E., Helm, R., Johnson, R., & Vlissides, J. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994.
- Feathers, M. Working Effectively with Legacy Code. Prentice Hall, 2004.
- Seemann, M., & van Deursen, S. Dependency Injection Principles, Practices, and Patterns. Manning, 2019.
- Meszaros, G. xUnit Test Patterns: Refactoring Test Code. Addison-Wesley, 2007.
- Bloch, J. Effective Java, 3rd edition. Addison-Wesley, 2017 — Item 73, Throw exceptions appropriate to the abstraction.
- Microsoft Learn. Task-based Asynchronous Pattern (TAP). https://learn.microsoft.com/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap
- Microsoft Learn.
DisplayAttributeClass. https://learn.microsoft.com/dotnet/api/system.componentmodel.dataannotations.displayattribute - Microsoft Learn. Use
IHttpClientFactoryto implement resilient HTTP requests. https://learn.microsoft.com/dotnet/core/extensions/httpclient-factory - Fowler, M. Mocks Aren't Stubs. https://martinfowler.com/articles/mocksArentStubs.html
- Harris, P. A., Taylor, R., Minor, B. L., Elliott, V., Fernandez, M., O'Neal, L., McLeod, L., Delacqua, G., Delacqua, F., Kirby, J., & Duda, S. N. The REDCap consortium: Building an international community of software platform partners. Journal of Biomedical Informatics, 95, 103208 (2019).
redcap-api/
├─ src/RedcapApi/ # library source
│ ├─ Api/ # RedcapApi.cs (facade impl), DefaultRedcapTransport.cs
│ ├─ Interfaces/ # IRedcap.cs, IRedcapTransport.cs, 17 domain interfaces
│ ├─ Models/ # enums (Content, RedcapFormat, RedcapAction, …) + DTOs
│ ├─ Exceptions/ # RedcapApiException
│ └─ Utils.cs # GetDisplayName extension + HttpClient helpers
├─ tests/RedcapApi.Tests/ # xUnit test project (~3,900 LOC across 11 files)
├─ docs/ # DocFX site sources
│ ├─ diagrams/ # SVG figures referenced from README
│ ├─ docfx.json
│ ├─ index.md
│ └─ toc.yml
├─ .github/ # GitHub config + upgrade scenario notes
├─ Directory.Build.props # TreatWarningsAsErrors, analyzers
├─ Directory.Packages.props # central package versioning
├─ RedcapApi.slnx # solution
├─ CONTRIBUTING.md
├─ LICENSE.md
└─ README.md # ← you are here
Alphabetical, useful for Ctrl-F:
CreateFolderFileRepositoryAsync · CreateProjectAsync · DeleteArmsAsync · DeleteDagsAsync · DeleteEventsAsync · DeleteFileAsync · DeleteFileRepositoryAsync · DeleteRecordsAsync · DeleteUserRolesAsync · DeleteUsersAsync · ExportArmsAsync · ExportDagsAsync · ExportEventsAsync · ExportFieldNamesAsync · ExportFileAsync · ExportFileFileRepositoryAsync · ExportFilesFoldersFileRepositoryAsync · ExportInstrumentMappingAsync · ExportInstrumentsAsync · ExportLoggingAsync · ExportMetaDataAsync · ExportPDFInstrumentsAsync · ExportProjectInfoAsync · ExportProjectXmlAsync · ExportRecordAsync · ExportRecordsAsync · ExportRedcapVersionAsync · ExportReportsAsync · ExportSurveyAccessCodeAsync · ExportSurveyLinkAsync · ExportSurveyParticipantsAsync · ExportSurveyQueueLinkAsync · ExportSurveyReturnCodeAsync · ExportUserDagAssignmentAsync · ExportUserRoleAssignmentAsync · ExportUserRolesAsync · ExportUsersAsync · GenerateNextRecordNameAsync · ImportArmsAsync · ImportDagsAsync · ImportEventsAsync · ImportFileAsync · ImportFileRepositoryAsync · ImportInstrumentMappingAsync · ImportMetaDataAsync · ImportProjectInfoAsync · ImportRecordsAsync · ImportUserDagAssignmentAsync · ImportUserRoleAssignmentAsync · ImportUserRolesAsync · ImportUsersAsync · RenameRecordAsync · SwitchDagAsync
Many of these have multiple overloads — see the DocFX site for the full set.
Most often-used enums and the wire values they emit through [Display(Name="…")]:
| Enum | Member | Wire value |
|---|---|---|
Content |
Record |
record |
Content |
MetaData |
metadata |
Content |
Arm |
arm |
Content |
Event |
event |
Content |
File |
file |
Content |
FileRepository |
fileRepository |
Content |
User |
user |
Content |
UserRole |
userRole |
Content |
Dag |
dag |
Content |
Project |
project |
Content |
Report |
report |
Content |
Log |
log |
Content |
Version |
version |
RedcapFormat |
json / csv / xml / odm |
json / csv / xml / odm |
RedcapAction |
Export / Import / Delete / Rename / Switch / Randomize |
export / import / delete / rename / switch / randomize |
OverwriteBehavior* |
normal / overwrite |
normal / overwrite |
* OverwriteBehavior does not use [Display] — its enum members are already lower-case, so Enum.ToString() produces the wire value directly.
End-to-end tests against a real REDCap instance are skipped by default. Set these to run them:
| Variable | Description | Example |
|---|---|---|
REDCAP_E2E_URL |
Full URL to your REDCap API endpoint. | https://redcap.example.edu/redcap/api/ |
REDCAP_E2E_TOKEN |
API token issued by your REDCap project. | 0123456789ABCDEF… |
REDCAP_E2E_RECORD_ID |
A valid record ID in the project. | 1 |
REDCAP_E2E_FORM |
A valid instrument / form name. | demographics |
Run only the E2E suite:
dotnet test tests/RedcapApi.Tests/RedcapApi.Tests.csproj --filter "Category=E2E"Or skip it explicitly during a non-E2E run:
dotnet test tests/RedcapApi.Tests/RedcapApi.Tests.csproj --filter "Category!=E2E"MIT — see LICENSE.md. © 2026 Curtin University.