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:

Feedback and questions are welcome on our forum, just as usual.