Search Results for

    Show / Hide Table of Contents

    Service Operations

    Service operations are the core mechanism you use in XData to add business logic to your server and later invoke it from clients.

    Service operations are invoked via HTTP requests, providing the correct URL and sending/receiving data via JSON payloads. This chapter explain hows to implement and use service operations.

    XData Service Wizard

    Easiest way to create service operations is by using the XData Service wizard.

    From the Delphi IDE, choose File > New > Other...

    From the dialog that appears, navigate to Delphi Projects > TMS Business and choose the wizard TMS XData Service.

    That will launch the "New XData Service" dialog, which will provide you with the following options:

    • Service Name: Specifies the base name for the service contract interface and service implementation class, as well the unit names to be generated.

    • Generate interface and implementation in separated units: If checked, service contract interface and service implementation classes will be created in two different units. This makes it easy to reuse the interface for use in client applications using TXDataClient. This is true by default. If you are not going to use interfaces at client-side or if you simply prefer to put all code in a single unit, uncheck this option.

    • Add sample methods: If checked, the wizard will add some sample methods (service operations) as an example.

    • Use a specific model: If checked, adds a Model attribute to the service contract/implementation with the name specified in the edit box.

    After options are set, click Finish and the full source code with the service contract and service implementation will be generated for you.

    If you prefer to create the source code manually, or simply want to learn more about the generated source code, refer to the following topics:

    • Service Operations Tutorial

    • Creating Service Contract

    • Service Implementation

    Service Operations Overview

    This chapter describes basic steps you need to follow to implement service operations at server side and invoke them from Delphi side. For detailed information about the steps, please refer to Service Operations main topic.

    The presented source code samples are not complete units and only display the relevant piece of code for understanding service operations usage. Some obvious code and keywords might have been removed to improve readability. Here we will implement two server operations, one using scalar types (Sum), and another using entities (FindOverduePayments).

    This tutorial also assumes that you have already created your XData Server application.

    A service operation is built by creating two elements:

    a. A Service Contract interface, which describes the methods and how they will be accessible from clients. This interface can be shared between server and client applications;

    b. A Service Implementation class, which effectively implement the service contract. This is used only by the server app.

    The following steps explains how to use service operations, including the creation of mentioned types. The XData Service Wizard automates steps 1 and 2 for you.

    1. Define a service contract interface and declare the operations (methods) in it.

    unit MyServiceInterface;
    
    uses
      {...}, XData.Service.Common;
    
    type
      [ServiceContract]
      IMyService = interface(IInvokable)
      ['{F0BADD7E-D4AE-4521-8869-8E1860B0A4A0}']
        function Sum(A, B: double): double;
        function FindOverduePayments(CustomerId: integer): TList<TPayment>;
      end;
    
    initialization
      RegisterServiceType(TypeInfo(IMyService));
    end.
    

    This will add the contract to the XData model. You can use the Model attribute to specify the model where the contract belongs to:

    uses {...}, Aurelius.Mapping.Attributes;
    {...}
    [ServiceContract]
    [Model('Sample')] // adds interface to "Sample" model
    IMyService = interface(IInvokable)
    {...}
    

    2. Create a service implementation class that implements the interface.

    uses
      {...}, MyServiceInterface, 
      XData.Server.Module,
      XData.Service.Common;
    
    type
      [ServiceImplementation]
      TMyService = class(TInterfacedObject, IMyService)
      private
        function Sum(A, B: double): double;
        function FindOverduePayments(CustomerId: integer): TList<TPayment>;
      end;
    
    implementation
    
    function TMyService.Sum(A, B: double): double;
    begin
      Result := A + B;
    end;
    
    function TMyService.FindOverduePayments(CustomerId: integer): TList<TPayment>;
    begin
      Result := TList<TPayment>.Create;
      TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
      // go to the database, instantiate TPayment objects, 
      // add to the list and fill the properties.
    end;
    
    initialization
      RegisterServiceType(TMyService);
    
    end.
    

    3. Run the XData server.

    It will automatically detect the service interfaces and implementations declared in the application. Your operations can now be invoked from clients. When the operation is invoked, the server creates an instance of the service implementation class, and invokes the proper method passing the parameters sent by the client.

    4. If using Delphi applications: invoke the operation using XData client.

    This tutorial assumes you created the xdata server at base address "http://server:2001/tms/xdata".

    uses
      {...}
      MyServiceInterface,
      XData.Client;
    
    var
      Client: TXDataClient;
      MyService: IMyService;
      SumResult: double;
      Payments: TList<TPayment>;
    begin
      Client := TXDataClient.Create;
      Client.Uri := 'http://server:2001/tms/xdata';
      MyService := Client.Service<IMyService>;
      SumResult := MyService.Sum(5, 10);
      try
        Payments := MyService.FindOverduePayments(5142);
      finally
        // process payments
        Payments.Free;
      end;
      Client.Free;
    end;
    

    5. If using non-Delphi applications: invoke the operation using HTTP, from any platform and/or development tool.

    Perform an HTTP Request:

    POST /tms/xdata/MyService/Sum HTTP/1.1
    Host: localhost:2001
    
    {
      "a": 5,
      "b": 8
    }
    

    And get the response:

    HTTP/1.1 200 OK
    
    {
      "value": 13
    }
    

    Creating Service Contract

    Service operations are grouped in service contracts, or service interfaces. You can define multiple service interfaces in your XData server, and each interface can define multiple operations (methods). You create a service contract by defining the service interface (a regular interface type to your Delphi application). The following topics in this chapter describes the steps and options you have to create such contract.

    Defining Service Interface

    These are the basic steps you need to follow to create an initial service contract interface:

    1. Declare an interface inheriting from IInvokable.

    2. Create a GUID for the interface (Delphi IDE creates a GUID automatically for you using Shift+Ctrl+G shortcut key).

    3. Add XData.Service.Common unit to your unit uses clause.

    4. Add [ServiceContract] attribute to your interface.

    5. Declare methods in your interface.

    6. Call RegisterServiceType method passing the typeinfo of your interface (usually you can do that in initialization section of your unit).

    When you add the unit where interface is declared to either your server or client application, XData will automatically detect it and add it to the XData model as a service contract, and define all interface methods as service operations. The client will be able to perform calls to the server, and the server just needs to implement the interface. The source code below illustrates how to implement the service interface.

    unit MyServiceInterface;
    
    interface
    
    uses
      System.Classes, Generics.Collections, 
      XData.Service.Common;
    
    type
      [ServiceContract]
      IMyService = interface(IInvokable)
      ['{BAD477A2-86EC-45B9-A1B1-C896C58DD5E0}']
        function Sum(A, B: double): double;
        function HelloWorld: string;
      end;
    
    implementation
    
    initialization
      RegisterServiceType(TypeInfo(IMyService));
    end.
    

    You can use the Model attribute to specify the model where the contract belongs to. This way you can have a server with multiple server modules and models.

    uses {...}, Aurelius.Mapping.Attributes;
    {...}
    [ServiceContract]
    [Model('Sample')] // adds interface to "Sample" model
    IMyService = interface(IInvokable)
    {...}
    

    Routing

    Each service operation is associated with an endpoint URL and an HTTP method. In other words, from clients you invoke a service by performing an HTTP method in a specific URL. The server mechanism of receiving an HTTP request and deciding which service operation to invoke is called routing.

    Note

    In all the examples here, the server base URL is assumed to be http://localhost:2001. Of course, use your own base URL if you use a different one.

    Default Routing

    By default, to invoke a service operation you would perform a POST request to the URL <service>/<action>, where <service> is the interface name without the leading "I" and <action> is the method name. For example, with the following service interface declaration:

    IMyService = interface(IInvokable)
      function Sum(A, B: double): double;
    

    you use the following request to invoke the Sum method:

    POST /MyService/Sum
    

    Note that the parameters follow a specific rule, they might be part of the routing, or not, depending on routing settings and parameter binding settings.

    Modifying the HTTP method

    The method can respond to a different HTTP method than the default POST. You can change that by using adding an attribute to method specifying the HTTP method the operation should respond to. For example:

    IMyService = interface(IInvokable)
      [HttpGet] function Sum(A, B: double): double;
    

    The above declaration will define that Sum method should be invoked using a GET request instead of a POST request:

    GET /MyService/Sum
    

    You can use attributes for other HTTP methods as well. You can use attributes: HttpGet, HttpPut, HttpDelete, HttpPatch and HttpPost (although this is not needed since it's default method).

    Using Route attribute to modify the URL Path

    By using the Route attribute, you can modify the URL path used to invoke the service operation:

    [Route('Math')]  
    IMyService = interface(IInvokable)
      [Route('Add')]
      function Sum(A, B: double): double;
    

    which will change the way to invoke the method:

    POST /Math/Add
    

    You can use multiple segments in the route, in both interface and method. For example:

    [Route('Math/Arithmetic')]  
    IMyService = interface(IInvokable)
      [Route('Operations/Add')]
      function Sum(A, B: double): double;
    

    will change the way to invoke the method:

    POST /Math/Arithmetic/Operations/Add
    

    An empty string is also allowed in both interface and method Route attributes:

    [Route('Math/Arithmetic')]  
    IMyService = interface(IInvokable)
      [Route('')]
      function Sum(A, B: double): double;
    

    The above method will be invoked this way:

    POST /Math/Arithmetic
    

    And also, the way you bind parameters can affect the endpoint URL as well. For example, the Route attribute can include parameters placeholders:

    [Route('Math')]  
    IMyService = interface(IInvokable)
      [Route('{A}/Plus/{B}')]
      function Sum(A, B: double): double;
    

    In this case, the parameters will be part of the URL:

    POST /Math/10/Plus/5
    

    Replacing the root URL

    By default XData returns a service document if a GET request is performed in the root URL. You can replace such behavior by simply using the Route attribute as usual, just passing empty strings to it:

    [ServiceContract]
    [Route('')]
    IRootService = interface(IInvokable)
    ['{80A69E6E-CA89-41B5-A854-DFC412503FEA}']
      [HttpGet, Route('')]
      function Root: TArray<string>;
    end;
    

    A quest to the root URL will invoke the IRootService.Root method:

    GET /
    

    You can of course use other HTTP methods like PUT, POST, and the TArray<string> return is just an example, you can return any type supported by XData, just like with any other service operation.

    Conflict with Automatic CRUD Endpoints

    TMS XData also provides endpoints when you use Aurelius automatic CRUD endpoints. Depending on how you define your service operation routing, you might end up with service operations responding to same endpoint URL as automatic CRUD endpoints. This might happen inadvertently, or even on purporse.

    When that happens XData will take into account the value specified in the RoutingPrecedence property to decide if the endpoint should invoke the service operation, or behave as the automatic CRUD endpoint.

    By default, the automatic CRUD endpoints always have precedence, starting with the entity set name. This means that if there is an entity set at URL Customers/, any subpath under that path will be handled by the automatic CRUD endpoint processor, even if it doesn't exist. For example, Customers/Dummy/ URL will return a 404 error.

    Setting RoutingPrecedence to TRoutingPrecedence.Service will change this behavior and allow you to override such endpoints from service operations. Any endpoint routing URL you define in service operations will be invoked, even if it conflicts with automatic CRUD endpoints.

    Parameter Binding

    Service operation parameters can be received from the HTTP request in three different modes:

    • From a property of a JSON object in request body (FromBody);

    • From the query part of the request URL (FromQuery);

    • From a path segment of the request URL (FromPath).

    In the method declaration of the service contract interface, you can specify, for each parameter, how they should be received, using the proper attributes. If you don't, XData will use the default binding.

    Default binding modes

    This are the rules used by XData to determine the default binding for the method parameters that do not have an explicity binding mode:

    1. If the parameter is explicitly used in a Route attribute, the default mode will be FromPath, regardless of HTTP method used:

    [Route('orders')]  
    IOrdersService = interface(IInvokable)
      [Route('approved/{Year}/{Month}')]
      [HttpGet] function GetApprovedOrdersByMonth(Year, Month: Integer): TList<TOrder>;
    

    Year and Month parameters should be passed in the URL itself, in proper placeholders:

    GET orders/approved/2020/6
    

    2. Otherwise, if the HTTP method is GET, the default mode will be FromQuery:

    [Route('orders')]  
    IOrdersService = interface(IInvokable)
      [Route('approved')]
      [HttpGet] function GetApprovedOrdersByMonth(Year, Month: Integer): TList<TOrder>;
    

    Meaning Year and Month parameters should be passed as URL query parameters:

    GET orders/approved/?Year=2020&Month=6
    

    3. Otherwise, for all other HTTP methods, the default mode will be FromBody:

    [Route('orders')]
    IOrdersService = interface(IInvokable)
      [Route('approved')]
      [HttpPost] function GetApprovedOrdersByMonth(Year, Month: Integer): TList<TOrder>;
    

    In this case, Year and Month parameters should be passed as properties of a JSON object in request body:

    POST orders/approved/
    
    {
      "Year": 2020,
      "Month": 6
    }
    

    FromBody parameters

    FromBody parameters are retrieved from a JSON object in the request body, where each name/value pair corresponds to one parameter. The pair name must contain the parameter name (the one declared in the method), and value contains the JSON representation of the parameter value, which can be either scalar property values, the representation of an entity, or the representation of a collection of entities.

    As specified above, FromBody parameters are the default behavior if the HTTP method is not GET and no parameter is explicitly declared in the Route attribute.

    Example:

    [HttpPost] function Multiply([FromBody] A: double; [FromBody] B: double): double;
    

    How to invoke:

    POST /tms/xdata/MathService/Multiply HTTP/1.1
    Host: server:2001
    
    {
      "a": 5,
      "b": 8
    }
    

    FromQuery parameters

    FromQuery parameters are retrieved from the query part of the request URL, using name=value pairs separated by "&" character, and the parameter value must be represented as URI literal.

    Note

    You can only use scalar property values, or objects that only have properties of scalar value types.

    If the Multiply operation of previous example was modified to respond to a GET request, the binding mode would default to FromQuery, according to the following example.

    You can add the [FromQuery] attribute to the parameter to override the default behavior.

    Example:

    [HttpPost] function Multiply([FromQuery] A: double; [FromQuery] B: double): double;
    

    How to invoke:

    POST /tms/xdata/MathService/Multiply?a=5&b=8 HTTP/1.1
    

    FromQuery is the default parameter binding mode for GET requests.

    You can also use DTOs as query parameters, as long the DTO only have properties of scalar types. In this case, each DTO property will be a separated query param. Suppose you have a class TCustomerDTO which has properties Id and Name, then you can declare the method like this:

        [HttpGet] function FindByIdOrName(Customer: TCustomerDTO): TList<TCustomer>;
    

    You can invoke the method like this:

    GET /tms/xdata/CustomerService/FindByIdOrName?Id=10&Name='Paul' HTTP/1.1
    
    Warning

    Using objects as parameters in query strings might introduce a breaking change in TMS Web Core applications using your XData server. If your TMS Web Core application was built a version of TMS XData below 5.2, the connection to the XData server will fail.

    If you have TMS Web Core client applications accessing your XData server, and you want to use DTOs in queries, recompile your web client applications using TMS XData 5.2 or later.

    FromPath parameters

    FromPath parameters are retrieved from path segments of the request URL. Each segment is a parameter value, retrieved in the same order where the FromPath parameters are declared in the method signature. As a consequence, it modifies the URL address used to invoke the operation. The following example modified the Multiply method parameters to be received from path.

    Example:

    [HttpGet] function Multiply([FromPath] A: double; [FromPath] B: double): double;
    

    How to invoke:

    GET /tms/xdata/MathService/Multiply/5/8 HTTP/1.1
    

    Parameter "A" was the first declared, thus it will receive value 5. Parameter "B" value will be 8. If the parameters are explicitly declared in the Route attribute, FromPath mode is the default mode. All the remaining parameters flagged with FromPath that were not included in the Route attribute must be added as additional segments.

    Example:

    [Route('{A}/Multiply')]  
    [HttpGet] function Multiply(A: double; [FromPath] B: double): double;
    

    How to invoke:

    GET /tms/xdata/MathService/5/Multiply/8 HTTP/1.1
    

    Mixing binding modes

    You can mix binding modes in the same method operation, as illustrated in the following example.

    Method declaration:

    procedure Process(
      [FromPath] PathA: Integer; 
      [FromQuery] QueryA: string;
      BodyA, BodyB: string;
      [FromQuery] QueryB: Boolean;
      [FromPath] PathB: string;
    ): double;
    

    How to invoke:

    POST /tms/xdata/MyService/Process/5/value?QueryA=queryvalue&QueryB=true HTTP/1.1
    
    {
      "BodyA": "one",
      "BodyB": "two"
    }
    

    Here are the values of each parameter received:

    • PathA: 5
    • QueryA: queryvalue
    • BodyA: one
    • BodyB: two
    • QueryB: true
    • PathB: value

    Methods with a single FromBody object parameter

    If the method has a single FromBody parameter (parameters with other binding type can exist) and that parameter is an object, for example:

    procedure UpdateCustomer(C: TCustomer);
    

    then clients must send the JSON entity representation of the customer directly in request body, without the need to wrap it with the parameter name (C, for example).

    Methods with a single FromBody scalar parameter

    If the method receives a single scalar parameter, for example:

    procedure ChangeVersion(const Version: string);
    

    In this case, clients can send the value of Version parameter in a name/value pair named "value", in addition to the original "Version" name/value pair. Both can be used.

    Supported Types

    When defining service interfaces, there are several types you can use for both parameter and return types of the operations.

    Scalar types

    You can use the regular simple Delphi types, such as Integer, String, Double, Boolean, TDateTime and TGUID, and its variations (like Longint, Int64, TDate, etc.). The server will marshal such values using the regular JSON representation of simple types. Variant type is also supported as long the variant value is one of the supported simple types (String, Integer, etc.).

    Examples:

    function Sum(A, B: double): double;
    function GetWorld: string;
    

    Enumerated and Set Types

    Enumerated types and set types are also supported. For example:

    type 
      TMyEnum = (First, Second, Third);
      TMyEnums = set of TMyEnum;
    
    procedure ReceiveEnums(Value: TMyEnums);
    

    Enumeration values are represented in JSON as strings containing the name of the value ("First", "Second", according to the example above). Sets are represented as a JSON array of enumeration values (strings).

    Simple objects - PODO (Plain Old Delphi Objects)

    Any Delphi object can be received and returned. The objects will be marshaled in the HTTP request using object representation in JSON format. One common case for simple objects is to use it as structure input/output parameters, or to use them as DTO classes.

    function ReturnClientDTO(Id: Integer): TClientDTO;
    function FunctionWithComplexParameters(Input: TMyInputParam): TMyOutputParam;
    

    Aurelius Entities

    If you use Aurelius, then you can also use Aurelius entities. Aurelius entities are a special case of simple objects. The mechanism is actually very similar to entity representation, with only minor differences (like representation of proxies and associated entities). The entities will be marshaled in the HTTP request using entity representation in JSON Format.

    Examples:

    function ProcessInvoiceAndReturnDueDate(Invoice: TInvoice): TDateTime;
    function AnimalByName(const Name: string): TAnimal;
    

    Generic Lists: TList<T>

    You can use TList<T> types when declaring parameters and result types of operations. The generic type T can be of any supported type. If T in an Aurelius entity, the the list will be marshaled in the HTTP request using the JSON representation for a collection of entities. Otherwise, it will simply be a JSON array containing the JSON representation of each array item.

    procedure HandleAnimals(Animals: TList<TAnimal>);
    function GetActiveCustomers: TList<TCustomer>;
    function ReturnItems: TList<string>;
    

    Generics arrays: TArray<T>

    Values of type TArray<T> are supported and will be serialized as a JSON array of values. The generic type T can be of any supported type.

    procedure ProcessIds(Ids: TArray<Integer>);
    

    TJSONAncestor (XE6 and up)

    For a low-level transfer of JSON data between client and server, you can use any TJSONAncestor descendant: TJSONObject, TJSONArray, TJSONString, TJSONNumber, TJSONBool, TJSONTrue, TJSONFalse and TJSONNull. The content of the object will be serialized/deserialized direct in JSON format.

    Example:

    procedure GenericFunction(Input: TJSONObject): TJSONArray;
    

    TCriteriaResult

    If you use Aurelius, you can return Aurelius TCriteriaResult objects, or of course a TList<TCriteriaResult> as a result type of a service operation. This is very handy to implement service operations using Aurelius projections.

    For example:

    function TTestService.ProjectedCustomers(NameContains: string): TList<TCriteriaResult>;
    begin
      Result := TXDataOperationContext.Current.GetManager
        .Find<TCustomer>
        .CreateAlias('Country', 'c')
        .SetProjections(TProjections.ProjectionList
          .Add(TProjections.Prop('Id').As_('Id'))
          .Add(TProjections.Prop('Name').As_('Name'))
          .Add(TProjections.Prop('c.Name').As_('Country'))
        )
        .Where(TLinq.Contains('Name', NameContains))
        .OrderBy('Name')
        .ListValues;
    end;
    

    The JSON representation for each TCriteriaResult object is a JSON object which one property for each projected value. The following JSON is a possible representation of an item of the list returned by the method above:

    {
      "Id": 4,
      "Name": "John",
      "Country": "United States"
    }
    

    TStrings

    Values of type TStrings are supported and will be serialized as JSON array of strings. Since TStrings is an abstract type, you should only use it as property type in objects and you should make sure the instance is already created by the object. Or, you can use it as function result, and when implementing you should also create an instance and return it. You cannot receive TStrings in service operation parameters.

    TStream

    You can also use TStream as param/result types in service operations. When you do that, XData server won't perform any binding of the parameter or result value. Instead, the TStream object will contain the raw content of the request (or response) message body. Thus, if you declare your method parameter as TStream, you will receive the raw message body of the client request in the server. If you use TStream as the result type of your method, the message body of the HTTP response send back to the client will contain the exact value of the TStream. This gives you higher flexibility in case you want to receive/send custom or binary data from/to client, such as images, documents, or just a custom JSON format.

    function BuildCustomDocument(CustomerId: integer): TStream;
    procedure ReceiveDocument(Value: TStream);
    

    For obvious reasons, when declaring a parameter as TStream type, it must be the only input parameter in method to be received in the body, otherwise XData won't be able to marshal the remaining parameters. For example, the following method declaration is invalid:

    // INVALID declaration
    [HttpPost] procedure ReceiveDocument(Value: TStream; NumPages: Integer);
    

    Since the method is Post, then NumPages will by default be considered to be received in the request body (FromBody attribute), which is not allowed. You can, however, receive the NumPages in the query part of from url path:

    // This is valid declaration
    [HttpPost] procedure ReceiveDocument(Value: TStream; [FromQuery] NumPages: Integer);
    

    Value will contain the raw request body, and NumPages will be received from the URL query string.

    Return Values

    When a service operation executes succesfully, the response is 200 OK for operations that return results or 204 No Content for operations without a return type. If the operation returns a value, it will also be in the same JSON format used for parameters. The value is sent by the server wrapped by a JSON object with a name/value pair named "value". The value of the "value" pair is the result of the operation. In the Multiply example, it would be like this:

    HTTP/1.1 200 OK
    
    {
        "value": 40
    }
    

    There are some exceptions for the general rule above:

    Methods with parameters passed by reference

    In this case, the result will be a JSON object where each property relates to a parameter passed by reference. If the method is also a function (returns a value), then an additional property with name "result" will be included. For example, suppose the following method:

    function TMyService.DoSomething(const Input: string; var Param1, Param2: Integer): Boolean;
    

    This will be a possible HTTP response from a call to that method:

    HTTP/1.1 200 OK
    
    {
        "result": True,
        "Param1": 50,
        "Param2": 30
    }
    

    Method which returns a single object

    If the method returns a single object parameter, for example:

    function FindCustomer(const Name: string): TCustomer;
    

    then the server will return the JSON entity representation of the customer in the response body, without wrapping it in the "value" name/value pair.

    Default Parameters

    You can declare service operations with default parameters. For example:

    function Hello(const Name: string = 'World'): string;
    

    This will work automatically if you are invoking this service operation from Delphi clients. However, it will not work if you invoke this service operation directly performing an HTTP request. In this case you would have to provide all the parameter values otherwise a server error will be raised informing that a parameter is missing.

    This is because the default value declared in the function prototype above is not available at runtime, but at compile time. That's why it works when calling from Delphi clients: the compiler will provide the default value automatically in the client call - but that's a client feature: the HTTP request will always have all parameter values.

    To make the above method also work from raw HTTP requests and do not complain about missing parameters, you will need to inform (again) the default value using the [XDefault] attribute. This way XData server will know what is the default value to be used in case the client did not provide it:

    function Hello([XDefault('World')] const Name: string = 'World'): string;
    

    For more than one parameter, just add one Default attribute for each default parameter:

    procedure DoSomething(
        [XDefault('Default')] Name: string = 'Default'; 
        [XDefault(0)] Value: Integer = 0
      );
    

    You only have to add attributes to the method declaration in the service contact interface. Attributes in methods in service implementation class will be ignored.

    Parameters validation

    You can apply validation attributes to parameters and DTO classes to make sure you receive parameters and classes with the expected values. This saves you from needing to manually add validation code and returning error messages to the clients.

    For example:

      [ValidateParams]
      [HttpGet] function ListCitiesByState(const [Required, MaxLength(2)] State: string): TList<TCity>;
    

    The method above adds validation attributes Required and MaxLength to the State parameter. If a client invokes the endpoint without providing the State parameter, or passing it as empty (null string), or even passing a value longer than 2 characters, XData will reject the request and answer with a status code 400. Also a detailed error message will be provided for the client in JSON format in request body indicating what's wrong and what should be fixed.

    When implementing your method, you can safely rely that the State parameter will have the valid value and won't need to worry about checking for wrong values.

    Note

    The validation attributes will only be applied if the ValidateParams attribute is applied to the method. Alternatively you can apply the ValidateParams attribute to the interface, which will make all parameters for all methods in the interface to be validated.

    When a parameter is class, then the object itself will also be validated, i.e., all the mapped members will also have the validation attributes applied:

      TFoo = class
      strict private
        [Range(1, MaxInt)] FId: Integer;
        [MaxLength(10)] FName: string;
      public
        property Id: Integer read FId write FId;
        property Name: string read FName write FName;
      end;
    
    {...}
    
      [ValidateParams] procedure AcceptFoo([Required] Foo: TFoo);
    

    In the above example, when AcceptFoo is invoked, XData will apply the Required validation to it. If Foo is nil, the request will be rejected. But also, even if Foo object is provided, the request will only be accepted if the Id property is positive, and Name property is not longer than 10 characters.

    For example, if the following JSON is sent to the endpoint:

    {
        "Id": 0,
        "Name": "ABCDEFGHIJKL"
    }
    

    The endpoint will answer with a 400 Bad Request response including the following detailed content:

    {
        "error": {
            "code": "ValidationFailed",
            "message": "Validation failed",
            "errors": [
                {
                    "code": "OutOfRange",
                    "message": "Field Id must be between 1 and 2147483647"
                },
                {
                    "code": "ValueTooLong",
                    "message": "Field Name must have no more than 10 character(s)"
                }
            ]
        }
    }
    

    For the complete reference of available validation attributes, please refer to the Data Validation chapter in TMS Aurelius documentation.

    Service Implementation

    Once you have defined the service interface, you need to write the server-side implementation of the service. To do this you must:

    1. Create a class implementing the service interface.

    2. Inherit the class from TInterfacedObject, or be sure the class implement automatic referencing counting for the interface.

    3. Add XData.Service.Common unit to your unit uses clause.

    4. Add [ServiceImplementation] attribute to your interface.

    5. Implement in your class all methods declared in the interface.

    6. Call RegisterServiceType method passing the implementation class (usually you can do that in initialization section of your unit).

    When you create a TXDataServerModule to build your XData server, it will automatically find all classes that implement interfaces in the model and use them when operations are invoked by the client. When a client invokes the operation, the server will find the class and method implementing the requested service through routing, will create an instance of the class, bind input parameters, and invoke the method. If the method returns a value, the server will bind the return values and send it back to the client. The instance of the implementation class will then be destroyed.

    The following source code illustrates how to implement the MyService interface defined in the topic "Defining Service Interface":

    unit MyService;
    
    interface
    
    uses
      System.Classes, Generics.Collections, 
      MyServiceInterface,
      XData.Service.Common;
    
    type
      [ServiceImplementation]
      TMyService = class(TInterfacedObject, IMyService)
      private
        function Sum(A, B: double): double;
        function HelloWorld: string;
      end;
    
    implementation
    
    function TMyService.Sum(A, B: double): double;
    begin
      Result := A + B;
    end;
    
    function TMyService.GetWorld: string;
    begin
      Result := 'Hello, World';
    end;
    
    initialization
      RegisterServiceType(TMyService);
    
    end.
    

    Server Memory Management

    When executing service operations, XData provides a nice automatic memory management mechanism that destroy all objects involved in the service operation execution. This makes it easier for you to implement your methods (since you mostly don't need to worry about object destruction) and avoid memory leaks, something that is critical at the server-side.

    In general, all objects are automatically destroyed by XData at server-side. This means that when implementing the service, you don't need to worry to destroy any object. However, it's important to know how this happens, so you implement your code correctly, and also knowing that in some specific situations of advanced operations, the previous statement might not be completely true. So strictly speaking, the objects automatically destroyed by XData are the following:

    • Any object passed as parameter or returned in a function result (called param object or result object);

    • Any object managed by the context TObjectManager;

    • The context TObjectManager is also destroyed after the operation method returns;

    • Any associated object that is also deserialized or serialized together with the param object or result object.

    Example

    Suppose the following meaningless service implementation:

    function TMyService.DoSomething(Param: TMyParam): TMyResult;
    var
      Entity: TMyEntity;
    begin
      Entity := TMyEntity.Create;
      Entity.SomeProperty := Param.OtherProperty;
      TXDataOperationContext.Current.GetManager.Save(Entity);
      Result := TMyResult.Create('test');
    end;
    

    You have three objects involved here:

    • Param, of type TMyParam and was created by XData;

    • function Result, of type TMyResult, being created and returned in the last line of service implementation;

    • Entity, of type TMyEntity, being created and then passed to context TObjectManager to be saved in the database.

    You don't need to destroy any of those objects. Whatever objects are passed as param (Param) of function result are always destroyed by XData automatically. The context manager is automatically destroyed by XData, and since you have added an entity to it, it will be destroyed as well upon manager destruction (this is a TMS Aurelius feature, not XData).

    Managed objects

    To control what will be destroyed and avoid double destruction of same object instance (for example, if the same object is received as parameter and returned as function result), XData keeps a collection of object instances in a property named ManagedObjects, available in the TXDataRequestHandler object. You can access that property using the operation context:

    TXDataOperationContext.Current.Handler.ManagedObjects
    

    You can also add object to that collection in advance, to make sure the object you are creating is going to be destroyed eventually by XData. This can help you out in some cases. For example, instead of using a construction like this:

    function TMyService.DoSomething: TMyResult;
    begin
      Result := TMyResult.Create;
      try
        // do complex operation with Result object
      except
        Result.Free;
        raise;
      end;
    end;
    

    You could write it this way:

    function TMyService.DoSomething: TMyResult;
    begin
      Result := TMyResult.Create;
      // Make sure Result will be eventually destroyed no matter what
      TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
    
      // now do complex operation with Result object
    end;
    

    Specific cases

    As stated before, in general you just don't need to worry, since the above rules cover pretty much all of your service implementation, especially if you use the context TObjectManager to perform database operations instead of creating your own manager. The situations where something might go wrong are the following:

    1. If you create your own TObjectManager and save/retrieve entities with it that were also passed as parameter or are being returned in function result.

    This will cause an Access Violation because when you destroy your object manager, the entities managed by it will be destroyed. But then XData server will also try to destroy those objects because they were passed as parameter or are being returned. To avoid this, you must set your TObjectManager.OwnsObjects property to false, or use the context TObjectManager so XData knows that the objects are in the manager and don't need to be destroyed. The latter option is preferred because it's more straightforward and more integrated with XData behavior.

    2. If you are creating associated objects that are not serialized/deserialized together with your object

    For example, suppose your method returns an object TCustomer. This TCustomer class has a property Foo: TFoo, but such property is not mapped in Aurelius as an association (or not marked to be serialized if TCustomer is a PODO). You then create a TFoo object, assign it to Foo property and return:

    Result := TCustomer.Create;
    Result.Foo := TFoo.Create;
    

    Since the Foo property will not be serialized by the server, XData will not know about such object, and it will not be destroyed. So you must be sure to destroy temporary/non-mapped/non-serializable objects if you create them (or add them to ManagedObjects collection as explained above). This is a very rare situation but it's important that developers be aware of it.

    There is a specific topic for client-side memory management using TXDataClient.

    TXDataOperationContext

    To help you implement your service operations, XData provides you some information under the context of the operation being executed. Such information is provided in a TXDataOperationContext object (declared in unit XData.Server.Module). The simplified declaration of such object is as followed.

    TXDataOperationContext = class
    public
      class function Current: TXDataOperationContext;
      function GetConnectionPool: IDBConnectionPool;
      function Connection: IDBConnection;
      function GetManager: TObjectManager;
      function CreateManager: TObjectManager; overload;
      function CreateManager(Connection: IDBConnection): TObjectManager; overload;
      function CreateManager(Connection: IDBConnection; Explorer: TMappingExplorer): TObjectManager; overload;
      function CreateManager(Explorer: TMappingExplorer): TObjectManager; overload;
      procedure AddManager(AManager: TObjectManager);
      function Request: THttpServerRequest;
      function Response: THttpServerResponse;
    end;
    

    To retrieve the current instance of the context, use the class function Current.

    Context := TXDataOperationContext.Current.GetManager;
    

    Then you can use the context to retrieve useful info, like a ready-to-use, in-context TObjectManager to manipulate Aurelius entities (using the GetManager function) or the connection pool in case you want to acquire a database connection and use it directly (using GetConnectionPool method).

    This way your service implementation doesn't need to worry about how to connect to the database or even to create a TObjectManager (if you use Aurelius) with proper configuration and destroy it at the end of process. All is being handled by XData and the context object. Another interesting thing is that when you use the TObjectManager provided by the context object, it makes it easier to manage memory (destruction) of entity objects manipulated by the server, since it can tell what objects will be destroyed automatically by the manager and what objects need to be destroyed manually. See Memory Management topic for more information.

    The example is an example of an operation implementation that uses context manager to retrieve payments from the database.

    uses 
      {...}
      XData.Server.Module;
    
    function TCustomerService.FindOverduePayments(CustomerId: integer): TList<TPayment>;
    begin
      Result := TXDataOperationContext.Current.GetManager.Find<TPayment>
        .CreateAlias('Customer', 'c')
        .Where(TLinq.Eq('c.Id', CustomerId) and TLinq.LowerThan('DueDate', Now))
        .List;
    end;
    

    Note that the database connection to be used will be the one provided when you created the TXDataServerModule. This allows you keep your business logic separated from database connection. You can even have several modules in your server pointing to different databases, using the same service implementations.

    Inspecting request and customizing response

    The context also provides you with the request and response objects provided by the TMS Sparkle framework.

    You can, for example, set the content-type of a stream binary response:

    function TMyService.GetPdfReport: TStream;
    begin
      TXDataOperationContext.Current.Response.Headers.SetValue('content-type', 'application/pdf');
      Result := InternalGetMyPdfReport;
    end;
    

    Or you can check for a specific header in the request to build some custom authentication system:

    function TMyService.GetAppointment(const Id: integer): TVetAppointment;
    var
      AuthHeaderValue: string;
    begin
      AuthHeaderValue := TXDataOperationContext.Current.Request.Headers.Get('custom-auth');
      if not CheckAuthorized(AuthHeaderValue) then
        raise EXDataHttpException.Create(401, 'Unauthorized'); // unauthorized
    
      // Proceed to normal request processing.
      Result := TXDataOperationContext.Current.GetManager.Find<TVetAppointment>(Id);
    end;
    

    The default connection

    The default database connection interface (IDBConnection) is provided in the property Connection:

    DefConnection := TXDataOperationContext.Current.Connection;
    

    It's the same connection used by the default manager, and it's retrieved from the pool when needed.

    Additional managers

    You might need to create more Aurelius managers in a service operation. You can of course do it manually, but if by any chance you want to return one entity inside one of those managers, you can't destroy the manager you've created because otherwise the object you want to return will also be destroyed.

    In these scenario, you can use the context methods CreateManager or AddManager. The former will create a new instance of a TObjectManager for you, and you can use it just like the default manager: find objects, save, flush, and don't worry about releasing it.

    function TSomeService.AnimalByName(const Name: string): TAnimal;
    var
      Manager: TObjectManager;
    begin
      Manager := TXDataOperationContext.Current.CreateManager(TMappingExplorer.Get('OtherModel'));
      Result := Manager.Find<TAnimal>
        .Where(TLinq.Eq('Name', Name)).UniqueResult;
    end;
    

    To create the manager you can optionally pass an IDBConnection (for the database connection), or the model (TMappingExplorer), or both, or none. It will use the default Connection if not specified, and it will use the model specified in the XData server (module) if model is not specified.

    If you want to create the manager yourself, you can still tell XData to destroy it when the request processing is finished, by using AddManager.

    function TSomeService.AnimalByName(const Name: string): TAnimal;
    var
      Manager: TObjectManager;
    begin
      Manager := TObjectManager.Create(SomeConnection, TMappingExplorer.Get('OtherModel'));
    
      TXDataOperationContext.Current.AddManager(Manager);
      Result := Manager.Find<TAnimal>
        .Where(TLinq.Eq('Name', Name)).UniqueResult;
    end;
    

    XData Query

    XData offers full query syntax to query entities from automatic CRUD endpoints. But you can also benefit from XData query mechanism in service operations. You can receive query information sent from the client and create an Aurelius criteria from it to retrieve data from database.

    Receiving query data

    To allow a service operation to receive query data like the automatic CRUD endpoint, declare a parameter of type TXDataQuery (declared in unit XData.Query):

      IMyService = interface(IInvokable)
        [HttpGet] function List(Query: TXDataQuery): TList<TCustomer>;
    

    The class TXDataQuery is declared like this (partial):

      TXDataQuery = class
      strict private
        [JsonProperty('$filter')]
        FFilter: string;
        [JsonProperty('$orderby')]
        FOrderBy: string;
        [JsonProperty('$top')]
        FTop: Integer;
        [JsonProperty('$skip')]
        FSkip: Integer;
    

    Which means clients can invoke the endpoint passing the same $filter, $orderby, $top and $skip parameters accepted also by the automatic CRUD endpoint, like this:

    /MyService/List/?$filter=Name eq 'Foo'&$orderby=Name&$top=10&$skip=30

    You can use the XData query builder do build the string for you:

      EndpointUrl := ServerBaseUrl + '/MyService/List/?' + 
        CreateQuery.From(TCustomer)
          .Filter(Linq['Name'] eq 'Foo')
          .OrderBy('Name')
          .Top(10).Skip(30)
          .QueryString;
      // Invoke EndpoingUrl using any HTTP client
    

    When invoking the service using the IMyService interface, you should pass the TXDataQuery object, either creating it directly:

      Query := TXDataQuery.Create('Name eq ''Foo''', 'Name', 10, 30);
      XClient.ReturnedEntities.Add(Query);
      Customer := Client.Service<IMyService>.List(Query);
    

    Or using the XData query builder:

      Query := CreateQuery
        .From(TCustomer)
        .Filter(Linq['Name'] = 'Foo')
        .OrderBy('Name')
        .Top(10).Skip(30)
        .Build;
      XClient.ReturnedEntities.Add(Query);
      Customer := Client.Service<IMyService>.List(Query);
    

    Creating a criteria

    From your service operation, you can then use the received TXDataQuery object to create an Aurelius criteria, using the CreateCriteria method of the current TXDataOperationContext:

    function TMyService.List(Query: TXDataQuery): TList<TCustomer>;
    begin
      Result := TXDataOperationContext.Current
        .CreateCriteria<TCustomer>(Query).List;
    end;
    

    You can of course create the criteria and then modify it as you wish. Since you are implementing a service operation, instead of using an automatic CRUD endpoint, you have full control of the business logic you want to implement. You could even, for example, create a criteria based on a query on a DTO object:

    function TMyService.List(Query: TXDataQuery): TList<TCustomer>;
    begin
      Result := TXDataOperationContext.Current
        .CreateCriteria<TCustomer>(Query, TCustomerDTO).List;
    end;
    

    In the example above, even though you are creating a criteria for the TCustomer class, the query will be validated from the TCustomerDTO class. This means XData will only accept queries that use properties from TCustomerDTO. For example, if the filter string is $filter=Status eq 2, even if the Status property exists in TCustomer, if it does not exist in class TCustomerDTO the query will not be accepted and an error "property Status does not exist" will return to the client.

    In This Article
    • XData Service Wizard
    • Service Operations Overview
    • Creating Service Contract
      • Defining Service Interface
      • Routing
        • Default Routing
        • Modifying the HTTP method
        • Using Route attribute to modify the URL Path
        • Replacing the root URL
        • Conflict with Automatic CRUD Endpoints
      • Parameter Binding
        • Default binding modes
        • FromBody parameters
        • FromQuery parameters
        • FromPath parameters
        • Mixing binding modes
        • Methods with a single FromBody object parameter
        • Methods with a single FromBody scalar parameter
      • Supported Types
        • Scalar types
        • Enumerated and Set Types
        • Simple objects - PODO (Plain Old Delphi Objects)
        • Aurelius Entities
        • Generic Lists: TList<T>
        • Generics arrays: TArray<T>
        • TJSONAncestor (XE6 and up)
        • TCriteriaResult
        • TStrings
        • TStream
      • Return Values
        • Methods with parameters passed by reference
        • Method which returns a single object
      • Default Parameters
      • Parameters validation
    • Service Implementation
    • Server Memory Management
      • Managed objects
      • Specific cases
    • TXDataOperationContext
      • Inspecting request and customizing response
      • The default connection
      • Additional managers
    • XData Query
      • Receiving query data
      • Creating a criteria
    Back to top TMS XData v5.20
    © 2002 - 2025 tmssoftware.com