Your first RPC call
Wire up SmartData.Client and invoke the procedure from your-first-procedure over binary RPC.
Same procedure as the previous page — usp_customer_list — now called from a separate console process over HTTP. The only functional changes are (a) opening an authenticated connection, and (b) SmartDataConnection.SendAsync in place of IProcedureService.ExecuteAsync. Everything else stays the same.
Prereqs:
- Your first procedure complete — the
HelloSmartDataserver is what we'll call. - Server running.
dotnet runit and note the port Kestrel prints (e.g.Now listening on: http://localhost:5219).
1. Why auth now matters
The previous page used IProcedureService — framework authority, auth gate bypassed, UserId = "system". POST /rpc is wired to IAuthenticatedProcedureService instead, so every incoming call needs a valid session token. SmartDataConnection handles login on OpenAsync; we just supply credentials in the connection string.
No change to the server is required. See Procedures → Two callers, one boundary for the trust split.
2. Create the client project
cd ..
dotnet new console -n HelloSmartData.Demo
cd HelloSmartData.Demo
dotnet add package SmartData.Client
3. Mirror the result DTO
GetData<T>() deserializes by property name, case-insensitive — the client-side type doesn't have to be the same class or even in the same namespace as the server's CustomerListResult. Minimal mirror at the bottom of Program.cs:
public class CustomerListResult
{
public List<Customer> Items { get; set; } = new();
public int Total { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string CompanyName { get; set; } = "";
public string? City { get; set; }
}
See Return DTOs, not entities for why production code should return a narrower DTO instead of the entity.
4. Program.cs — open, then call
Replace Program.cs (substitute <port> with the port the server is listening on):
using SmartData.Client;
await using var conn = new SmartDataConnection(
"Server=http://localhost:<port>;User Id=admin;Password=admin");
await conn.OpenAsync(); // performs sp_login, stores the token
var listResp = await conn.SendAsync("usp_customer_list", new()
{
["Database"] = "master",
["Search"] = "acme",
});
if (!listResp.Success)
throw new Exception($"{listResp.Error} (id={listResp.ErrorId})");
var result = listResp.GetData<CustomerListResult>()!;
foreach (var c in result.Items)
Console.WriteLine($"{c.Id,3} {c.CompanyName,-12} {c.City}");
Five things worth naming:
- Connection string drives auth.
Server,User Id,Password(or a pre-existingToken=) are the only knobs.OpenAsynccallssp_loginfor you and remembers the token for every subsequentSendAsync. await usingcloses cleanly. Disposal callssp_logoutso the server can release the session.new()is a target-typedDictionary<string, object>—SendAsynctakes the dictionary as the args bag. PassDatabasehere when the target procedure needs it.- Every response carries
Success/Error/ErrorId/ErrorSeverity— switch on those rather than parsing message strings. Details in Binary RPC. - If the session is no longer valid (TTL expiry, admin revoke),
SendAsyncthrowsSmartDataExceptionand the connection transitions toBroken. Open a new one to recover. Sessions are persisted to_sys_sessions, so a server restart does not invalidate tokens.
Append the DTO classes from step 3 and save.
5. Run both
# terminal 1 — the server from the previous page
cd ../HelloSmartData
dotnet run
# terminal 2 — this client
cd ../HelloSmartData.Demo
dotnet run
Expected output:
1 Acme Corp Springfield
3 Acme Labs Portland
Same two rows the previous page returned from /demo, now fetched over HTTP.
What just happened
- Client binary-serialized a
CommandRequest { Command = "usp_customer_list", Token, Args }— theArgsdictionary itself is binary-serialized and holdsDatabase = "master"alongsideSearch = "acme"— then POSTed it to/rpc. - Server's
CommandRouterdeserialized, validated the token, and handed off toProcedureExecutor. ProcedureExecutoropened a DI scope, instantiatedCustomerList, boundSearchby name, calledExecute.- Result binary-serialized into
CommandResponse.Data;GetData<CustomerListResult>()deserialized it on this end.
Full picture: Architecture → Request lifecycle.
Where to go next
- Four-procedure CRUD. Build a CRUD app extends this with save, delete, and DTO folder layout.
- Mental model. Binary RPC — wire format, call flow, error fields.
- API surface. SmartData.Client reference — the full
SmartDataConnectionsurface.