GnuHealth.Client
0.1.0
dotnet add package GnuHealth.Client --version 0.1.0
NuGet\Install-Package GnuHealth.Client -Version 0.1.0
<PackageReference Include="GnuHealth.Client" Version="0.1.0" />
<PackageVersion Include="GnuHealth.Client" Version="0.1.0" />
<PackageReference Include="GnuHealth.Client" />
paket add GnuHealth.Client --version 0.1.0
#r "nuget: GnuHealth.Client, 0.1.0"
#:package GnuHealth.Client@0.1.0
#addin nuget:?package=GnuHealth.Client&version=0.1.0
#tool nuget:?package=GnuHealth.Client&version=0.1.0
GnuHealth.Net
A .NET client for building applications on top of GNU Health (the Tryton-based HMIS/EMR).
GnuHealth.Net talks to GNU Health through the trytond JSON-RPC API β the same interface the official desktop and web clients use β rather than reaching into the PostgreSQL database directly. Every read and write runs through the real ORM, so business logic, validation, code sequences, access control and workflow transitions are enforced server-side. Your integrations get a clean, typed API and cannot leave the database in a state the application can't handle.
- π Safe by construction β no hand-reimplemented ORM logic, no schema corruption.
- β¨οΈ Strongly typed β 498 POCOs covering the entire model catalog, generated from a live server.
- π§© Untyped escape hatch β work with any model/field dynamically when you need to.
- π§ͺ Testable & DI-friendly β interface seams, a fakeable transport,
IServiceCollectionwiring. - π― Broad reach β
netstandard2.0core (usable from .NET Framework 4.6.1+, .NET 5β8, MAUI, Unity).
Contents
- Why JSON-RPC and not SQL?
- Architecture
- Getting started
- Usage
- Core concepts
- Model coverage & code generation
- Build, test, pack
- Good to know (GNU Health specifics)
- Security
- Roadmap
Why JSON-RPC and not SQL?
You can write to GNU Health's PostgreSQL database directly β but to do it safely you would have to re-implement, per model and per version, everything the ORM does:
- code sequences (
prescription_id, evaluation/lab codes, party PUIDs, β¦), create/writeoverrides andvalidate()rules,- workflow state machines (surgery
draftβconfirmedβdoneβsigned, β¦), - computed/function fields,
ir_model_data, history tables,ir.property, - access rules, record rules and translations.
Going through the JSON-RPC API means all of that is handled for you by trytond. The trade-off is that bulk reads are a little slower than raw SQL β so for heavy reporting/BI, read-only SQL is still fine; just keep all writes on the API.
Architecture
Clean, inward-pointing dependencies (the domain contracts have no dependencies of their own):
GnuHealth.Abstractions
(interfaces, value types, attributes, Domain, WriteCommand, exceptions)
β² β²
β β
GnuHealth.Client GnuHealth.Models GnuHealth.Codegen
transport Β· serialization generated POCOs introspects a live server,
ORM Β· mapping Β· sessions (by module ns) emits GnuHealth.Models
configuration Β· DI
| Project | Target | Responsibility |
|---|---|---|
GnuHealth.Abstractions |
netstandard2.0 | Contracts and value types: IGnuHealthConnection, IModelClient, ITypedModelClient<T>, IRecordMaterializer, ITrytonRpcClient; TrytonDate, TrytonRecord, Domain, WriteCommand, [TrytonModel]/[TrytonField] |
GnuHealth.Client |
netstandard2.0 | Implementation, split by concern: Rpc/ (JSON-RPC transport), Serialization/ (Tryton __class__ value codec), Orm/ (ModelClient, TypedModelClient), Mapping/ (reflection materializer), Sessions/, Configuration/, DependencyInjection/ |
GnuHealth.Models |
netstandard2.0 | 498 generated POCOs grouped into GnuHealth.Models.<Module> namespaces |
GnuHealth.Codegen |
net8.0 | CLI that reads fields_get from a live server and emits the POCOs |
samples/GnuHealth.Sample |
net8.0 | Runnable example (env-based credentials) |
tests/GnuHealth.Client.Tests |
net8.0 | xUnit unit tests + env-gated live integration tests |
The layering follows SOLID: one responsibility per type, dependencies expressed as interfaces (DIP),
and swappable seams (ITrytonRpcClient, IRecordMaterializer) β which is how the ORM layer is
unit-tested without a live server.
Getting started
Prerequisites
- A reachable GNU Health / Tryton server with the JSON-RPC service enabled (default port
8000). - A Tryton login β ideally a dedicated, scoped service account rather than
admin. - .NET SDK 8.0+ to build.
Add the libraries. They aren't on nuget.org yet; reference the projects (or the .nupkgs built by
dotnet pack, see below):
<ItemGroup>
<ProjectReference Include="path/to/GnuHealth.Client/GnuHealth.Client.csproj" />
<ProjectReference Include="path/to/GnuHealth.Models/GnuHealth.Models.csproj" />
</ItemGroup>
Provide credentials via environment (never hard-code them):
GNUHEALTH_URL=http://your-host:8000
GNUHEALTH_DB=your-database
GNUHEALTH_USER=api_svc
GNUHEALTH_PASSWORD=β’β’β’β’β’β’β’β’
Usage
Connect
using GnuHealth.Client;
using GnuHealth.Client.Configuration;
var o = GnuHealthOptions.FromEnvironment(); // or set BaseUrl/Database/Login/Password directly
using var conn = new GnuHealthConnection(o.BaseUrl, o.Database);
Console.WriteLine(await conn.ServerVersionAsync()); // e.g. "7.0.45"
await conn.LoginAsync(o.Login, o.Password); // opens a session; loads user context
Typed models
using GnuHealth.Abstractions;
using GnuHealth.Models.Health; // module namespaces: .Health .Party .Product .Account .Stock .Ir ...
var patients = conn.Model<Patient>();
// read one (specify fields to keep it light; defaults to all mapped fields)
Patient? p = await patients.GetAsync(5, new[] { "rec_name", "blood_type", "rh" });
// search -> POCOs
IReadOnlyList<Patient> group = await patients.SearchAsync(
Domain.Where("blood_type", "=", "O"),
limit: 20,
order: new object[] { new object[] { "id", "ASC" } },
fields: new[] { "rec_name", "gender", "blood_type" });
Untyped model access
Any of the 500 models, no POCO required β values come back as CLR types
(long, string, bool, decimal, TrytonDate, DateTime, byte[], nested records):
var lab = conn.Model("gnuhealth.lab");
long[] ids = await lab.SearchAsync(Domain.Where("patient", "=", 5L), limit: 10);
IReadOnlyList<TrytonRecord> rows = await lab.ReadAsync(ids, new[] { "name", "test.rec_name", "state" });
foreach (var r in rows)
Console.WriteLine($"{r.GetString("name")} {r.GetRelated("test")?["rec_name"]} {r.GetString("state")}");
Creating & updating (writes)
Creates run every server-side default, validation and sequence:
long labId = await conn.Model("gnuhealth.lab").CreateOneAsync(new Dictionary<string, object?>
{
["test"] = 25L, // many2one -> id
["patient"] = 5L,
["date_requested"] = DateTime.UtcNow, // encoded as a Tryton datetime
["state"] = "draft",
});
await conn.Model("gnuhealth.lab").WriteAsync(new[] { labId }, new Dictionary<string, object?>
{
["diagnosis"] = "Reviewed",
});
// typed create/update round-trips the POCO's writable fields:
var disease = new PatientDisease { Patient = 5, Pathology = 2077, Status = "c", DiagnosedDate = new TrytonDate(2026, 1, 15) };
long id = await conn.Model<PatientDisease>().CreateAsync(disease); // disease.Id is stamped
disease.Status = "h";
await conn.Model<PatientDisease>().UpdateAsync(disease, new[] { "status" });
Relational writes (one2many / many2many)
Use WriteCommand to build the command tuples trytond expects:
await conn.Model("gnuhealth.lab").WriteAsync(new[] { labId }, new Dictionary<string, object?>
{
["critearea"] = new object[]
{
WriteCommand.Create(new Dictionary<string, object?> { ["name"] = "Glucose", ["result"] = 92m }),
WriteCommand.Add(5, 6), // link existing rows
WriteCommand.Remove(9), // unlink (does not delete)
},
});
Buttons & workflow
await conn.Model("gnuhealth.surgery").ButtonAsync("confirmed", new[] { surgeryId });
Dependency injection
using GnuHealth.Client.DependencyInjection;
services.AddGnuHealthClient(o =>
{
o.BaseUrl = cfg["GNUHEALTH_URL"]!;
o.Database = cfg["GNUHEALTH_DB"]!;
o.Login = cfg["GNUHEALTH_USER"]!;
o.Password = cfg["GNUHEALTH_PASSWORD"]!;
});
// later:
var factory = provider.GetRequiredService<IGnuHealthConnectionFactory>();
using IGnuHealthConnection conn = await factory.ConnectAsync(); // creates + logs in
More capabilities
// Resolve many2one relations to POCOs (single or batched, avoiding N+1):
var doctor = await conn.LoadAsync<Healthprofessional>(patient.PrimaryCareDoctor);
var doctors = await conn.LoadManyAsync<Healthprofessional>(patients.Select(p => p.PrimaryCareDoctor));
// Page through every match:
var all = await conn.Model<Patient>().SearchAllAsync(Domain.Where("blood_type", "=", "O"), pageSize: 500);
// Run a report -> bytes:
ReportResult label = await conn.ExecuteReportAsync("party.label", new long[] { partyId });
File.WriteAllBytes($"{label.Name}.{label.Type}", label.Content);
// Drive a wizard (low-level, stateful):
var wiz = await conn.CreateWizardAsync("gnuhealth.lab.test.create");
await wiz.ExecuteAsync(wiz.StartState, data);
await wiz.DeleteAsync();
Core concepts
Domainβ a search filter.Domain.All,Domain.Where("f","=",v).And("g",">",1), or combine:Domain.Combine("OR", Domain.Where(...), Domain.Where(...)).WriteCommandβCreate/Write/Add/Remove/Deletetuples for relational fields.TrytonDateβ a date without time, for Trytondatefields (DateTimemaps todatetime).TrytonRecordβ an untyped row with typed accessors (GetString,GetId,GetDate,GetRelatedfor dotted reads like"test.rec_name").- Context β the user's language/company context is loaded at login and merged into every call;
pass a per-call
contextdictionary to override. - Value marshalling β trytond encodes
date/datetime/Decimal/bytes/timedeltaas tagged objects; the client converts them to/from native CLR types transparently. - Typed errors β failures surface as
TrytonUserError(validation/business rules, safe to show users),TrytonUserWarning,TrytonConcurrencyException,TrytonAuthenticationExceptionandTrytonTransportExceptionβ all deriving fromTrytonRpcException. - Resilient sessions β an expired session (HTTP 401) is transparently re-authenticated and the
call retried once. Transient transport failures (network / 502 / 503 / 504 / 429) are retried with
exponential backoff (
GnuHealthOptions.MaxRetries). Via DI, connections use a pooledHttpClient(IHttpClientFactory), so creating many connections won't exhaust sockets. Pass anILogger(or anILoggerFactoryvia DI) to log calls, retries and re-auth. - Selection enums β Tryton
selectionfields with a static option list are generated as typed enums (e.g.Patient.BloodTypeKind.O,Patient.GenderKind.Female) mapped to their wire values via[TrytonValue]. (Symbol-only options such as Rh+/-get generic member names β the value is still preserved on the attribute.)
Model coverage & code generation
GnuHealth.Models is generated from a live server, so it always matches that instance's version and
customizations. This build covers 498 models / ~7,000 fields, grouped by module:
| Namespace | Models | Namespace | Models | |
|---|---|---|---|---|
GnuHealth.Models.Health |
201 | GnuHealth.Models.Stock |
40 | |
GnuHealth.Models.Account |
94 | GnuHealth.Models.Product |
22 | |
GnuHealth.Models.Ir |
82 | GnuHealth.Models.Party |
20 | |
GnuHealth.Models.Calendar |
13 | GnuHealth.Models.Res |
11 | |
| Country Β· Currency Β· Company Β· Webdav | 15 |
Regenerate against your own server β either from source, or install the published tool:
# as a global dotnet tool:
dotnet tool install -g GnuHealth.Codegen
gnuhealth-codegen http://host:8000 db admin PASSWORD --all --out ./Generated
# or from source (whole catalog; run as admin so admin-only models introspect fully):
dotnet run --project src/GnuHealth.Codegen -- http://host:8000 db admin PASSWORD \
--all --out src/GnuHealth.Models/Generated
# or a subset:
dotnet run --project src/GnuHealth.Codegen -- http://host:8000 db api_svc PASSWORD \
--models gnuhealth.patient,gnuhealth.lab --out src/GnuHealth.Models/Generated
Generating a POCO doesn't grant access β a record is still only readable/writable if the connected user's groups permit it.
Build, test, pack
dotnet build
# Unit tests always run. Live integration tests run only when GNUHEALTH_* env vars are set:
dotnet test
# Produce NuGet packages into ./artifacts:
dotnet pack -c Release -o artifacts
Good to know (GNU Health specifics)
common.server.versionis served at the server root and is unauthenticated; the client routes it there automatically (all model calls go to the per-database endpoint).- Functional fields may not be searchable. For example
gnuhealth.patient.genderis computed and has no searcher β you can read it but not filter on it; filter on stored fields (e.g.blood_type) or the underlyingparty.party.gender. - Empty many2one comes back as
false/null; typed properties are nullable and map it tonull.
Security
- Use a dedicated, scoped service account (not
admin). Grant it only the groups your product needs; audit trails then attribute changes to that account. - Keep credentials in environment variables / user-secrets / a secret store β never in source.
Publishing
Two independent release streams, driven by tags (see .github/workflows/ci.yml):
| Tag | Publishes | Versioning |
|---|---|---|
v1.2.3 |
GnuHealth.Abstractions, GnuHealth.Client, GnuHealth.Codegen |
the client libraries' own semver |
models-v5.0.1 |
GnuHealth.Models |
tracks the GNU Health release it was generated from |
So consumers can pin GnuHealth.Models to their server's GNU Health version, e.g.
<PackageReference Include="GnuHealth.Models" Version="5.0.*" />.
Publishing uses NuGet Trusted Publishing (OIDC) β no stored API key. One-time setup:
- nuget.org β your account β Trusted Publishing β add a policy: repository owner + repo,
workflow file
ci.yml(filename only). - Add a repo secret
NUGET_USER= your nuget.org account/profile name.
The workflow requests id-token: write, runs NuGet/login@v1 to mint a 1-hour key, and pushes.
GnuHealth.Models is generated from a stock GNU Health install, so it's an accurate baseline for
standard deployments. If your server runs extra/fewer modules or has custom fields, regenerate with the
gnuhealth-codegen tool to match it exactly β an unused generated POCO is harmless, but custom fields
won't be present until you regenerate. Cut a new models-v* release whenever GNU Health releases a new
version (regenerate against a stock server of that version, commit, tag).
Roadmap
- Change-tracking for partial typed updates;
IAsyncEnumerablestreaming for large reads. - Richer enum member naming for symbol-only selections; typed wizard state models.
- A published NuGet feed (CI already builds/tests/packs and publishes on tags).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- GnuHealth.Abstractions (>= 0.1.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.2)
- System.Text.Json (>= 8.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 120 | 7/13/2026 |