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 TXData
Client . 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 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:
Declare an interface inheriting from IInvokable.
Create a GUID for the interface (Delphi IDE creates a GUID automatically for you using Shift+Ctrl+G shortcut key).
Add
XData.Service.Common
unit to your unit uses clause.Add [ServiceContract] attribute to your interface.
Declare methods in your interface.
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:
Create a class implementing the service interface.
Inherit the class from TInterfacedObject, or be sure the class implement automatic referencing counting for the interface.
Add
XData.Service.Common
unit to your unit uses clause.Add [ServiceImplementation] attribute to your interface.
Implement in your class all methods declared in the interface.
Call RegisterServiceType method passing the implementation class (usually you can do that in initialization section of your unit).
When you create a TXData
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 TXData
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 TXData
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.