TXDataClient
The TXDataClient object (declared in unit XData.Client
) allows you to
send and receive objects to/from a XData server in a high-level,
easy-to-use, strong-typed way. From any platform, any development
environment, any language, you can always access XData just by using
HTTP and JSON, but if you are coding from Delphi client, TXDataClient
makes it much easier to write client applications that communicate with
XData server.
To start using a TXDataClient, you just need to instantiate it and set the Uri property to point to root URL of the XData server:
uses {...}, XData.Client;
{...}
Client := TXDataClient.Create;
Client.Uri := 'http://server:2001/tms/xdata';
// <use client>
Client.Free;
The following topics explain how to use TXDataClient in details.
Invoking Service Operations
You can easily invoke service operations from a Delphi application using the TXDataClient class. Even though XData implements service operations using standards HTTP and JSON, which allows you to easily invoke service operations using HTTP from any client or platform, the TXDataClient makes it even easier by providing strong typing and direct method calls that in turn perform the HTTP requests under the hood.
Another advantage is that you don't need to deal building or finding out the endpoint URL (routing), with binding parameters or even create client-side proxy classes (when you use Aurelius entities). The same service interface you defined for the server can be used in the client. You can share the units containing the service interfaces between client and server and avoid code duplication.
To invoke service operations, you just need to:
Retrieve an interface using Service<I> method.
Call methods from the interface.
Here is an example of invoking the method Sum of interface IMyService, declaring in the topic "defining service interface":
uses
{...}
MyServiceInterface,
XData.Client;
var
Client: TXDataClient;
MyService: IMyService;
SumResult: double;
begin
// Instantiate TXDataClient
Client := TXDataClient.Create;
// Set server Uri
Client.Uri := 'http://server:2001/tms/xdata';
// Retrieve IMyService inteface
MyService := Client.Service<IMyService>;
// call inteface methods
SumResult := MyService.Sum(5, 10);
end;
Note that the client won't destroy any object passed as parameters, and will only destroy entities created by it (that were returned from the server), but no regular objects (like TList<T> or TStream). See "Memory Management" topic for detailed information.
Client Memory Management
Since method operations can deal with several types of objects, either Aurelius entities, plain old Delphi objects or even normal classes like TList<T> or TStream, it's important to know exactly how XData handles the lifetime of those objects, in order to avoid memory leaks or access violation exceptions due to releasing the same object multiple times.
This is the behavior or TXDataClient when it comes to receiving/sending objects (there is a separated topic for server-side memory management).
Any object sent to the server (passed as a parameter) is not destroyed. You must handle the lifetime of those objects yourself.
Any object of type TStream or TList<T> returned from the server is not destroyed. You must handle the lifetime of those objects yourself.
Any other object returned from the server which type is not the ones mentioned in the previous item is automatically destroyed by default.
So consider the example below:
var
Customer: TCustomer;
Invoices: TList<TInvoice>;
{...}
Invoices := Client.Service<ISomeService>.DoSomething(Customer);
Customer.Free;
Invoices.Free;
Customer object is being passed as parameter. It will not be destroyed by the client and you must destroy it yourself. This is the same if you call Post, Put or Delete methods.
Items object (TList<T>) is being returned from the function. You must destroy the list yourself, it's not destroyed by the client. It's the same behavior for List method.
The TInvoice objects that are inside the list will be destroyed automatically by the client. You must not destroy them. Also, the same behavior for Get and List methods - entities are also destroyed by the client.
Alternatively, you can disable automatic management of entity instances at the client side, by using the ReturnedInstancesOwnership property:
Client.ReturnedInstancesOwnership := TInstanceOwnership.None;
The code above will prevent the client from destroying the object instances. You can also retrieve the list of all objects created by the client (that are supposed to be destroyed automatically) using property ReturnedEntities, in case you need to destroy them manually:
for Entity in Client.ReturnedEntities do {...}
Working With CRUD Endpoints
The following topics describe how to use TXDataClient to deal with TMS Aurelius CRUD Endpoints.
Requesting a Single Entity
To request a single entity, use the Get generic method passing the Id of the object as parameter:
Customer := Client.Get<TCustomer>(10);
State := Client.Get<TState>('FL');
The Id parameter is of type TValue, which has implicit conversions from some types like integer and string in the examples above. If there is no implicit conversion from the type of the id, you can use an overloaded method where you pass the type of Id parameter:
var
InvoiceId: TGuid;
begin
{ ... get invoice Id }
Invoice := Client.Get<TInvoice, TGuid>(InvoiceId);
end;
You can use the non-generic version of Get in case you only know the entity type at runtime (it returns a TObject and you need to typecast it to the desired type):
Customer := TCustomer(Client.Get(TCustomer, 10)));
Requesting an Entity List
Use the List method to query and retrieve a list of entities from the server:
var
Fishes: TList<TFish>;
begin
Fishes := Client.List<TFish>;
The TXDataClient.List<T> function will always create and retrieve an object of type TList<T>. By default you must manually destroy that list object later, as explained in memory management topic.
Optionally you can provide a query string to send to the server to perform filtering, order, etc., using the XData query options syntax:
Customers := Client.List<TCustomer>('$filter=(Name eq ''Paul'') or (Birthday lt 1940-08-01)&$orderby=Name desc');
Use the non-generic version in case you only know the type of the entity class at runtime. In this case, the function will create and return an object of type TList<TObject>:
var
Fishes: TList<TObject>;
begin
Fishes := XClient.List(TFish);
You also can use Count method to retrieve only the total number of entities without needing to retrieve the full entity list:
var
TotalFishes: Integer;
TotalCustomers: Integer;
begin
TotalFishes := Client.Count(TFish);
TotalCustomers := Client.Count(TCustomer, '$filter=(Name eq ''Paul'') or (Birthday lt 1940-08-01)&$orderby=Name desc');
end;
Creating Entities
Use TXDataClient.Post to create a new object in the server.
C := TCountry.Create;
try
C.Name := 'Germany';
Client.Post(C);
finally
C.Free;
end;
Pay attention to client memory management to learn which objects you need to manually destroy. In this case, the client won't destroy the TCountry object automatically so you need to destroy it yourself.
The client makes sure that after a successful Post call, the Id of the object is properly set (if generated by the server).
Updating Entities
Use TXDataClient.Put to update an existing object in the server.
Customer := Client.Get<TCustomer>(10);
Customer.City := 'London'; // change city
Client.Put(Customer); // send changes
Pay attention to client memory management to learn which objects you need to manually destroy. Client won't destroy objects passed to Put method. In the above example, though, the object doesn't need to be destroyed because it was previously retrieved with Get, and in this case (for objects retrieved from the server), the client will manage and destroy it.
Removing Entities
Use TXDataClient.Delete to delete an object from the server. The parameter must be the object itself:
Customer := Client.Get<TCustomer>(10);
Client.Delete(Customer); // delete customer
Pay attention to client memory management to learn which objects you need to manually destroy. Client won't destroy objects passed to Delete method. In the above example, though, the object doesn't need to be destroyed because it was previously retrieved with Get, and in this case (for objects retrieved from the server), the client will manage and destroy it.
Using the Query Builder
XData allows you to easily query entities using a full query syntax, either by directly sending HTTP requests to entity set endpoints, or using the List
method of TXDataClient
.
For example, to query for customers which name is "Paul" or birthday date is lower then August 1st, 1940, ordered by name in descending order, you can write a code like this:
Customers := Client.List<TCustomer>('$filter=(Name eq ''Paul'') or (Birthday lt 1940-08-01)&$orderby=Name desc');
Alternatively to manually writing the raw query string yourself, you can use the XData Query Builder. The above code equivalent would be something like this:
uses {...}, XData.QueryBuilder, Aurelius.Criteria.Linq;
Customers := Client.List<TCustomer>(
CreateQuery
.From(TCustomer)
.Filter(
(Linq['Name'] = 'Paul')
or (Linq['Birthday'] < EncodeDate(1940, 8, 1))
)
.OrderBy('Name', False)
.QueryString
);
Filter and FilterRaw
The Filter
method receives an Aurelius criteria expression to later convert it to the syntax of XData $filter
query parameter. Please refer to Aurelius criteria documentation to learn more about how to build such queries. A quick example:
CreateQuery.From(TCustomer)
.Filter(Linq['Name'] = 'Paul')
.QueryString
Will result in $filter=Name eq Paul
. You can also write the raw query string directly using FilterRaw
method:
CreateQuery.From(TCustomer)
.FilterRaw('Name eq Paul')
.QueryString
OrderBy and OrderByRaw
Method OrderBy
receives either a property name in string format, or an Aurelius projection. A second optional boolean parameter indicates if the order must be ascending (True
, the default) or descending (False
). For example:
CreateQuery.From(TCustomer)
.OrderBy('Name')
.OrderBy('Id', False)
.QueryString
Results in $orderby=Name,Id desc
. The overload using Aurelius project allows for more complex expressions, like:
CreateQuery.From(TCustomer)
.OrderBy(Linq['Birthday'].Year)
.QueryString
Which results in $orderby=year(Birthday)
. You can also write the raw order by expression directly using OrderByRaw
method:
CreateQuery.From(TCustomer)
.OrderByRaw('year(Birthday)')
.QueryString
Top and Skip
Use Top and Skip methods to specify the values of $top
and $skip
query options:
CreateQuery.Top(10).Skip(30)
.QueryString
Results in $top=10&$skip=30
.
Expand
Specifies the properties to be added to $expand
query option:
CreateQuery.From(TInvoice)
.Expand('Customer')
.Expand('Product')
.QueryString
Results in $expand=Customer,Product
.
Subproperties
If you need to refer to a subproperty in either Filter
, OrderBy
or Expand
methods, just separate the property names using dot (.
):
CreateQuery.From(TCustomer)
.Filter(Linq['Country.Name'] = 'Germany')
.QueryString
Results in $filter=Country/Name eq 'Germany'
.
From
If your query refers to property names, you need to use the From
method to specify the base type being queried. This way the query builder will validate the property names and check their types to build the query string properly. There are two ways to do so: passing the class of the object being queries, or the entity type name:
CreateQuery.From(TCustomer) {...}
CreateQuery.From('Customer') {...}
Note that you can also specify the name of an instance type, ie., an object that is not necessarily an Aurelius entity, but any Delphi object that you might be passing as a DTO parameter.
When you pass a class name, the query builder will validate agains the names of field and properties of the class, not the final JSON value. For example, suppose you have a class mapped like this:
TCustomerDTO = class
strict private
FId: Integer;
[JsonProperty('the_name')]
FName: string;
{...}
The following query will work ok:
CreateQuery.From('Customer').Filter(Linq['the_name'] = 'Paul')
While the following query will fail:
CreateQuery.From(TCustomerDTO).Filter(Linq['the_name'] = 'Paul')
Because the_name
is not a valid property name for TCustomerDTO
class. The correct query should be:
CreateQuery.From(TCustomerDTO).Filter(Linq['Name'] = 'Paul')
Which will then result in the query string $filter=the_name eq 'Paul'
.
Client and Multi-Model
When you create the TXDataClient object, it uses the default entity model to retrieve the available entity types and service operations that can be retrieved/invoked from the server. When your server has multiple models, though, you need to specify the model you are using when accessing the server. This is useful for the client to know which service interface contracts it can invoke, and of course, the classes of entities it can retrieve from the server. To do that, you pass the instance of the desired model to the client constructor:
Client := TXDataClient.Create(TXDataAureliusModel.Get('Security'));
See topic "Multiple servers and models" for more information.
Authentication Settings
For the HTTP communication, TXDataClient uses under the hood the Sparkle THttpClient. Such object is accessible through the TXDataClient.HttpClient property. You can use all properties and events of THttpClient class there, and the most common is the OnSendingRequest event, which you can use to set custom headers for the request. One common usage is to set the Authorization header with credentials, for example, a JSON Web Token retrieved from the server:
XDataClient.HttpClient.OnSendingRequest :=
procedure(Req: THttpRequest)
begin
Req.Headers.SetValue('Authorization', 'Bearer ' + vToken);
end;
Legacy Basic authentication
TXDataClient class provides you with the following properties for accessing servers protected with basic authentication.
property UserName: string;
property Password: string;
Defines the UserName and Password to be used to connect to the server. These properties are empty by default, meaning the client won't send any basic authentication info. This is equivalent to set the Authorization header with property basic authentication value.
Ignoring Unknown Properties
TMS XData allows you work with the entity and DTO classes at client-side. Your client application can be compiled with the same class used in the server, and when a response is received from the server, the class will be deserialized at client-side.
However, it might happen that your server and client classes get out of sync. Suppose you have a class TCustomer both server and client-side. The server serializes the TCustomer, and client deserializes it. At some point, you update your server adding a new property TCustomer.Foo. The server then sends the JSON with an additional Foo property, but the client was not updated and it doesn't recognize such property, because it was compiled with an old version of TCustomer class.
By default, the client will raise an exception saying Foo property is not known. This is the safest approach since if the client ignore the property, it might at some point send the TCustomer back to the server without Foo, and such property might get cleared in an update, for example.
On the other hand, this will require you to keep your clientes updated and in sync with the server to work. If you don't want that behavior, you can simply tell the client to ignore properties unknown by the client. To do this, use the IgnoreUnknownProperties property from TXDataClient:
XDataClient1.IgnoreUnknownProperties := True;