# 16. Client-Server Services via Interfaces
Service-Oriented Architecture Made Simple
In Chapter 14, we covered method-based services — a direct approach with full HTTP control. This chapter introduces interface-based services, mORMot's powerful SOA implementation that provides automatic client stub generation, contract validation, multiple instance lifetimes, and bidirectional communication via WebSockets.
Method-based services have limitations:
| Feature | Description |
|---|---|
| Design by Contract | Interfaces define the service contract in pure Pascal |
| Auto Marshalling | JSON serialization handled automatically |
| Factory Driven | Get implementations from interfaces on both client and server |
| Multiple Lifetimes | Per-call, shared, per-session, per-user, per-group, client-driven |
| Contract Validation | Client/server compatibility verified before execution |
| Bidirectional | Callback interfaces for real-time notifications |
| Secure | Per-method authorization via user groups |
| Cross-Platform | Generated client code for Delphi, FPC, JavaScript |
type
ICalculator = interface(IInvokable)
['{9A60C8ED-CEB2-4E09-87D4-4A16F496E5FE}']
/// add two signed 32-bit integers
function Add(n1, n2: Integer): Integer;
/// multiply two 64-bit integers
function Multiply(n1, n2: Int64): Int64;
end;
Requirements:
IInvokable (ensures RTTI)register calling convention (default)| Type | Serialization |
|---|---|
Boolean |
JSON true/false |
Integer, Cardinal, Int64, Double, Currency |
JSON number |
| Enumerations | JSON number (ordinal value) |
| Sets | JSON number (bitmask, up to 32 elements) |
TDateTime, TDateTimeMS |
ISO 8601 JSON string |
RawUtf8, string, UnicodeString |
JSON string (UTF-8) |
RawJson |
JSON passthrough (no escaping) |
RawByteString |
Base64-encoded JSON string |
TPersistent, TOrm |
JSON object (published properties) |
TObjectList |
JSON array with "ClassName" field |
| Dynamic arrays | JSON array |
record |
JSON object (with RTTI or custom serialization) |
variant, TDocVariant |
Native JSON |
TServiceCustomAnswer |
Custom response (binary, HTML, etc.) |
interface |
Callback for bidirectional communication |
function Process(const Input: RawUtf8; // Client → Server only
var InOut: Integer; // Client ↔ Server (both ways)
out Output: RawUtf8 // Server → Client only
): Boolean; // Server → Client (result)
type
IComplexService = interface(IInvokable)
['{8B5A2B10-7B3C-4A7D-95F3-8C9D7E6A5B4C}']
// Simple types
function Calculate(n1, n2: Double): Double;
// Record parameters
function ProcessOrder(const Order: TOrderRecord): TOrderResult;
// Dynamic arrays
function FilterItems(const Items: TRawUtf8DynArray;
const Filter: RawUtf8): TRawUtf8DynArray;
// Object parameters (caller allocates)
procedure TransformCustomer(var Customer: TCustomer);
// Variant/TDocVariant for flexible JSON
function QueryData(const Params: Variant): Variant;
// Custom binary response
function GetReport(ReportID: Integer): TServiceCustomAnswer;
end;
For non-JSON responses (PDF, images, HTML):
function TMyService.GetReport(ReportID: Integer): TServiceCustomAnswer;
begin
Result.Header := HEADER_CONTENT_TYPE + 'application/pdf';
Result.Content := GeneratePDF(ReportID);
Result.Status := HTTP_SUCCESS;
end;
Note: Methods returning TServiceCustomAnswer cannot have var or out parameters.
type
TServiceCalculator = class(TInterfacedObject, ICalculator)
public
function Add(n1, n2: Integer): Integer;
function Multiply(n1, n2: Int64): Int64;
end;
function TServiceCalculator.Add(n1, n2: Integer): Integer;
begin
Result := n1 + n2;
end;
function TServiceCalculator.Multiply(n1, n2: Int64): Int64;
begin
Result := n1 * n2;
end;
// Using TypeInfo
Server.ServiceRegister(TServiceCalculator, [TypeInfo(ICalculator)], sicShared);
// Or using registered interface directly
Server.ServiceDefine(TServiceCalculator, [ICalculator], sicShared);
| Mode | Description | Thread Safety |
|---|---|---|
sicSingle |
New instance per call (default, safest) | Not required |
sicShared |
One instance for all calls (fastest) | Required |
sicClientDriven |
Instance lives until client releases interface | Not required |
sicPerSession |
One instance per authentication session | Required |
sicPerUser |
One instance per user across sessions | Required |
sicPerGroup |
One instance per user group | Required |
sicPerThread |
One instance per server thread | Not required |
| Use Case | Recommended Mode |
|---|---|
| Stateless operations, resource-intensive | sicSingle |
| Simple stateless service, high throughput | sicShared |
| Workflow with state between calls | sicClientDriven |
| Session-specific caching | sicPerSession |
| User preferences/settings | sicPerUser |
| Group-level configuration | sicPerGroup |
| Thread-local resources (e.g., database connection) | sicPerThread |
type
IComplexNumber = interface(IInvokable)
['{29D753B2-E7EF-41B3-B7C3-827FEB082DC1}']
procedure Assign(aReal, aImaginary: Double);
function GetReal: Double;
procedure SetReal(const Value: Double);
function GetImaginary: Double;
procedure SetImaginary(const Value: Double);
procedure Add(aReal, aImaginary: Double);
property Real: Double read GetReal write SetReal;
property Imaginary: Double read GetImaginary write SetImaginary;
end;
TServiceComplexNumber = class(TInterfacedObject, IComplexNumber)
private
fReal, fImaginary: Double;
public
procedure Assign(aReal, aImaginary: Double);
function GetReal: Double;
procedure SetReal(const Value: Double);
function GetImaginary: Double;
procedure SetImaginary(const Value: Double);
procedure Add(aReal, aImaginary: Double);
end;
// Registration
Server.ServiceDefine(TServiceComplexNumber, [IComplexNumber], sicClientDriven);
The server maintains fReal and fImaginary between calls until the client releases the interface.
The recommended approach — inherit from TInjectableObjectRest:
type
TMyService = class(TInjectableObjectRest, IMyService)
public
function GetCurrentUser: RawUtf8;
procedure LogActivity(const Action: RawUtf8);
end;
function TMyService.GetCurrentUser: RawUtf8;
begin
if Server <> nil then
Result := Server.SessionGetUser(Factory.CurrentSession).LogonName
else
Result := '';
end;
procedure TMyService.LogActivity(const Action: RawUtf8);
begin
Server.Add(TOrmActivityLog, [
'Action', Action,
'User', GetCurrentUser,
'Timestamp', NowUtc
]);
end;
Properties available:
Server: TRestServer — Access to ORM and server methodsFactory: TServiceFactoryServer — Service factory instance
For services not inheriting from TInjectableObjectRest:
function TMyService.ProcessRequest: RawUtf8;
var
Ctxt: PServiceRunningContext;
begin
Ctxt := PerThreadRunningContextAddress;
if Ctxt^.Request <> nil then
Result := Ctxt^.Request.SessionUserName
else
Result := 'Unknown';
end;
Note: Prefer TInjectableObjectRest — it's safer and works outside client-server context.
// Must match server-side mode
Client.ServiceRegister([TypeInfo(ICalculator)], sicShared);
// Or with registered interface
Client.ServiceDefine([ICalculator], sicShared);
var
Calc: ICalculator;
begin
if Client.Services.Resolve(ICalculator, Calc) then
ShowMessage(IntToStr(Calc.Add(10, 20)));
end;
Generic syntax (Delphi 2010+):
var
Calc: ICalculator;
begin
Calc := Client.Service<ICalculator>;
if Calc <> nil then
ShowMessage(IntToStr(Calc.Add(10, 20)));
end;
var
CN: IComplexNumber;
begin
if Client.Services.Resolve(IComplexNumber, CN) then
begin
CN.Assign(0.01, 3.14);
CN.Add(100, 200);
ShowMessage(Format('%.2f + %.2fi', [CN.Real, CN.Imaginary]));
end;
end; // CN released here → server instance also released
For sicClientDriven, explicit registration is optional:
// This works without prior ServiceRegister call
var
CN: IComplexNumber;
begin
Client.Services.Info(IComplexNumber).Get(CN); // Auto-registers as sicClientDriven
end;
By default, mORMot generates an MD5 hash of the interface signature:
For explicit version control:
// Server
Server.ServiceRegister(TMyService, [TypeInfo(IMyService)], sicShared)
.SetOptions([], 'v2.5'); // Contract = 'v2.5'
// Client must match
Client.ServiceRegister([TypeInfo(IMyService)], sicShared, 'v2.5');
This allows:
var
Factory: TServiceFactoryServer;
begin
Factory := Server.Services.Info(ICalculator) as TServiceFactoryServer;
// Deny all by default
Factory.DenyAll;
// Allow specific groups
Factory.Allow(ICalculator, [ADMIN_GROUP_ID]);
// Allow specific methods for other groups
Factory.AllowByName(['Add', 'Multiply'], [USER_GROUP_ID]);
end;
For public methods:
Server.ServiceMethodByPassAuthentication('Calculator.GetVersion');
Factory.SetOptions([optExecInMainThread]); // Execute in main VCL thread
Factory.SetOptions([optFreeInMainThread]); // Free instance in main thread
Factory.SetOptions([optExecInPerInterfaceThread]); // Dedicated thread per interface
Factory.SetServiceLog(Server, TOrmServiceLog);
This logs:
Interface.Method)type
TOrmMyServiceLog = class(TOrmServiceLog)
published
property CustomField: RawUtf8 read fCustomField write fCustomField;
end;
Factory.SetServiceLog(Server, TOrmMyServiceLog);
type
// Callback interface (client implements this)
IProgressCallback = interface(IInvokable)
['{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}']
procedure Progress(Percent: Integer; const Status: RawUtf8);
procedure Completed(Success: Boolean);
end;
// Service interface
ILongRunningTask = interface(IInvokable)
['{12345678-1234-1234-1234-123456789012}']
procedure StartTask(const TaskName: RawUtf8; const Callback: IProgressCallback);
procedure CancelTask(const TaskID: RawUtf8);
end;
type
TLongRunningTask = class(TInjectableObjectRest, ILongRunningTask)
public
procedure StartTask(const TaskName: RawUtf8; const Callback: IProgressCallback);
procedure CancelTask(const TaskID: RawUtf8);
end;
procedure TLongRunningTask.StartTask(const TaskName: RawUtf8;
const Callback: IProgressCallback);
begin
// Start background work
TThread.CreateAnonymousThread(
procedure
var
i: Integer;
begin
for i := 0 to 100 do
begin
Sleep(100);
Callback.Progress(i, Format('Processing %s...', [TaskName]));
end;
Callback.Completed(True);
end
).Start;
end;
type
TMyProgressCallback = class(TInterfacedCallback, IProgressCallback)
private
fForm: TForm;
public
constructor Create(aForm: TForm; aRest: TRestClientUri);
procedure Progress(Percent: Integer; const Status: RawUtf8);
procedure Completed(Success: Boolean);
end;
constructor TMyProgressCallback.Create(aForm: TForm; aRest: TRestClientUri);
begin
inherited Create(aRest, IProgressCallback); // Register callback
fForm := aForm;
end;
procedure TMyProgressCallback.Progress(Percent: Integer; const Status: RawUtf8);
begin
TThread.Queue(nil,
procedure
begin
fForm.ProgressBar.Position := Percent;
fForm.StatusLabel.Caption := Status;
end);
end;
Callbacks require WebSocket transport:
// Server
HttpServer := TRestHttpServer.Create('8080', [Server], '+', useHttpAsync);
HttpServer.WebSocketsEnable(Server, 'privatekey');
// Client
Client := TRestHttpClientWebSockets.Create('localhost', '8080', Model);
Client.WebSocketsConnect('privatekey');
Client.ServiceDefine([ILongRunningTask], sicShared);
procedure TMyOtherService.DoSomething;
var
Calc: ICalculator;
begin
if Resolve(ICalculator, Calc) then
Result := Calc.Add(10, 20);
end;
procedure TMyOtherService.DoSomething;
var
Calc: ICalculator;
begin
Calc := Server.Service<ICalculator>;
if Calc <> nil then
Result := Calc.Add(10, 20);
end;
unit ProjectInterface;
interface
uses
mormot.core.base,
mormot.core.interfaces;
type
ICalculator = interface(IInvokable)
['{9A60C8ED-CEB2-4E09-87D4-4A16F496E5FE}']
function Add(n1, n2: Integer): Integer;
function Multiply(n1, n2: Int64): Int64;
end;
const
ROOT_NAME = 'api';
PORT_NAME = '8080';
implementation
initialization
TInterfaceFactory.RegisterInterfaces([TypeInfo(ICalculator)]);
end.
program Server;
{$APPTYPE CONSOLE}
uses
mormot.core.base,
mormot.orm.core,
mormot.rest.server,
mormot.rest.memserver,
mormot.rest.http.server,
ProjectInterface;
type
TServiceCalculator = class(TInterfacedObject, ICalculator)
public
function Add(n1, n2: Integer): Integer;
function Multiply(n1, n2: Int64): Int64;
end;
function TServiceCalculator.Add(n1, n2: Integer): Integer;
begin
Result := n1 + n2;
end;
function TServiceCalculator.Multiply(n1, n2: Int64): Int64;
begin
Result := n1 * n2;
end;
var
Model: TOrmModel;
Server: TRestServerFullMemory;
HttpServer: TRestHttpServer;
begin
Model := TOrmModel.Create([], ROOT_NAME);
Server := TRestServerFullMemory.Create(Model);
try
Server.ServiceDefine(TServiceCalculator, [ICalculator], sicShared);
HttpServer := TRestHttpServer.Create(PORT_NAME, [Server], '+', useHttpAsync);
try
WriteLn('Server running on http://localhost:', PORT_NAME);
WriteLn('Press Enter to stop...');
ReadLn;
finally
HttpServer.Free;
end;
finally
Server.Free;
Model.Free;
end;
end.
program Client;
{$APPTYPE CONSOLE}
uses
mormot.core.base,
mormot.orm.core,
mormot.rest.client,
mormot.rest.http.client,
ProjectInterface;
var
Model: TOrmModel;
Client: TRestHttpClientSocket;
Calc: ICalculator;
begin
Model := TOrmModel.Create([], ROOT_NAME);
Client := TRestHttpClientSocket.Create('localhost', PORT_NAME, Model);
try
Client.ServiceDefine([ICalculator], sicShared);
if Client.Services.Resolve(ICalculator, Calc) then
begin
WriteLn('10 + 20 = ', Calc.Add(10, 20));
WriteLn('10 * 20 = ', Calc.Multiply(10, 20));
end
else
WriteLn('Service not available');
finally
Client.Free;
Model.Free;
end;
end.
Interface-based services in mORMot 2 provide:
TInjectableObjectRest| Previous | Index | Next |
|---|---|---|
| Chapter 15: Interfaces and SOLID Design | Index | Chapter 17: Cross-Platform Clients |