Implementing service contract
In fact, the sample type as stated previously can be used directly:
type TServiceCalculator = class(TInterfacedObject, ICalculator) public function Add(n1,n2: integer): integer; end;
function TServiceCalculator.Add(n1, n2: integer): integer; begin result := n1+n2; end;
And... That is all we need. The Delphi IDE will check at compile time that
the class really implements the specified interface
definition, so
you'll be sure that your code meets the service contract expectations. Exact
match (like handling type of parameters) will be checked by the framework when
the service factory will be initialized, so you won't face any runtime
exception due to a wrong definition.
Here the class inherits from TInterfacedObject
, but you could
use any plain Delphi class: the only condition is that it implements the
ICalculator
interface.
Set up the Server factory
In order to have a working service, you'll need to initialize a server-side factory, as such:
Server.ServiceRegister(TServiceCalculator,[TypeInfo(ICalculator)],sicShared);
The Server
instance can be any TSQLRestServer
inherited class, implementing any of the supported protocol of
mORMot's communication protocol (direct call, GDI message, named
pipes, HTTP/TCP-IP).
The code line above will register the TServiceCalculator
class
to implement the ICalculator
service, with a single shared
instance life time (specified via the sicShared
parameter). An
optional time out value can be specified, in order to automatically release a
deprecated instance after some inactivity.
Whenever a service is executed, an implementation class is to be available.
The life time of this implementation class is defined on both client and server
side, by specifying a TServiceInstanceImplementation
value. This
setting must be the same on both client and server sides (it will be checked by
the framework).
Instances life time implementation
The available instance management options are the following:
Lifetime | Description |
sicSingle |
One class instance is created per call: - This is the most expensive way of implementing the service, but is safe for simple workflows (like a one-type call); - This is the default setting for TSQLRestServer.ServiceRegister
method. |
sicShared |
One object instance is used for all incoming calls and is not recycled subsequent to the calls - the implementation should be thread-safe on the server side |
sicClientDriven |
One object instance will be created in synchronization with the client-side
lifetime of the corresponding interface: when the interface will be released on
client (either when it comes out of scope or set to nil ), it will
be released on the server side - a numerical identifier will be transmitted
with all JSON requests |
sicPerSession |
One object instance will be maintained during the whole running session |
sicPerUser |
One object instance will be maintained and associated with the running user |
sicPerGroup |
One object instance will be maintained and associated with the running user's authorization group |
Of course, sicPerSession
, sicPerUser
and
sicPerGroup
modes will expect a specific user to be authenticated.
Those implementation patterns will therefore only be available if the RESTful
authentication is enabled between client and server.
Typical use of each mode may be the following:
Lifetime | Use case |
sicSingle |
An asynchronous process (may be resource consuming) |
sicShared |
Either a very simple process, or requiring some global data |
sicClientDriven |
The best candidate to implement a Business Logic workflow |
sicPerSession |
To maintain some data specific to the client application |
sicPerUser |
Access to some data specific to one user |
sicPerGroup |
Access to some data shared by a user category (e.g. administrator, or guests) |
In the current implementation of the framework, the class instance is allocated in memory.
This has two consequences:
- In client-server architecture, it is very likely that a lot of such instances
will be created. It is therefore mandatory that it won't consume a lot of
resource, especially with long-term life time: e.g. you should not store any
BLOB within these instances, but try to restrict the memory use to the minimum.
For a more consuming operation (a process which may need memory and CPU power),
the sicSingle
mode is preferred.
- There is no built-in data durability yet: service implementation shall ensure
that data remaining in memory (e.g. in sicShared
,
sicPerUser
or sicPerGroup
mode) won't be missing in
case of server shutdown. It is up to the class to persist the needed data -
using e.g. 3.
In order to illustrate sicClientDriven
implementation mode,
let's introduce the following interface and its implementation (extracted from
the supplied regression tests of the framework):
type IComplexNumber = interface(IInvokable) ['{29D753B2-E7EF-41B3-B7C3-827FEB082DC1}'] procedure Assign(aReal, aImaginary: double); function GetImaginary: double; function GetReal: double; procedure SetImaginary(const Value: double); procedure SetReal(const Value: double); procedure Add(aReal, aImaginary: double); property Real: double read GetReal write SetReal; property Imaginary: double read GetImaginary write SetImaginary; end;
Purpose of this interface is to store a complex number within its internal
fields, then retrieve their values, and define a "Add
" method, to
perform an addition operation. We used properties, with associated getter and
setter methods, to provide object-like behavior on Real
and
Imaginary
fields, in the code.
This interface is implemented on the server side by the following class:
type TServiceComplexNumber = class(TInterfacedObject,IComplexNumber) private fReal: double; fImaginary: double; function GetImaginary: double; function GetReal: double; procedure SetImaginary(const Value: double); procedure SetReal(const Value: double); public procedure Assign(aReal, aImaginary: double); procedure Add(aReal, aImaginary: double); property Real: double read GetReal write SetReal; property Imaginary: double read GetImaginary write SetImaginary; end;
{ TServiceComplexNumber }
procedure TServiceComplexNumber.Add(aReal, aImaginary: double); begin fReal := fReal+aReal; fImaginary := fImaginary+aImaginary; end;
procedure TServiceComplexNumber.Assign(aReal, aImaginary: double); begin fReal := aReal; fImaginary := aImaginary; end;
function TServiceComplexNumber.GetImaginary: double; begin result := fImaginary; end;
function TServiceComplexNumber.GetReal: double; begin result := fReal; end;
procedure TServiceComplexNumber.SetImaginary(const Value: double); begin fImaginary := Value; end;
procedure TServiceComplexNumber.SetReal(const Value: double); begin fReal := Value; end;
This interface is registered on the server side as such:
Server.ServiceRegister(TServiceComplexNumber,[TypeInfo(IComplexNumber)],sicClientDriven);
Using the sicClientDriven
mode, also the client side will be
able to have its own life time handled as expected. That is, both
fReal
and fImaginary
field will remain allocated on
the server side as long as needed.
Continue reading...
This article is part of a list of other blog articles, extracted from the official Synopse mORMot framework documentation:- Interface based services;
- Defining a data contract;
- Service side implementation;
- Using services on the Client or Server sides;
- Interface based services implementation details.
- WCF, mORMot and Event Sourcing.
Feedback and questions are welcome on our forum, just as usual.