A TMS XData service operation is a method on an interface marked
[ServiceContract]. XData publishes each one as a URL, and an attribute on the method
decides which HTTP method that URL answers to. The choice affects where the parameters travel,
what ends up in log files, what proxies may cache, and which clients break if you change it
later.
Everything below was checked against a running XData server built with Delphi 13: a small service hosted on Sparkle's Indy server, called with plain HTTP requests.
unit Demo_Service;
interface
uses
XData.Service.Common;
type
TOrderRequest = class
public
Product: string;
Quantity: Integer;
end;
[ServiceContract]
IDemoService = interface(IInvokable)
['{6C1A3E3B-6F1D-4E5A-9B7E-2D2D7E1F4A11}']
// No attribute: what does XData use?
function Server_Time: string;
// Reads data, safe to repeat, parameters are not sensitive
[HttpGet]
function Square(const N: Integer): Integer;
// Changes state
[HttpPost]
function Place_Order(const Order: TOrderRequest): Integer;
// Returns nothing
[HttpPost]
procedure Log_Out(const Session_Ref: Int64);
end;
[ServiceImplementation]
TDemoService = class(TInterfacedObject, IDemoService)
public
function Server_Time: string;
function Square(const N: Integer): Integer;
function Place_Order(const Order: TOrderRequest): Integer;
procedure Log_Out(const Session_Ref: Int64);
end;
implementation
uses
System.SysUtils, XData.Server.Module;
function TDemoService.Server_Time: string;
begin
Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', Now);
end;
function TDemoService.Square(const N: Integer): Integer;
begin
TXDataOperationContext.Current.Response.Headers.SetValue('Cache-Control', 'no-store');
Result := N * N;
end;
function TDemoService.Place_Order(const Order: TOrderRequest): Integer;
begin
Result := Order.Quantity * 10;
end;
procedure TDemoService.Log_Out(const Session_Ref: Int64);
begin
// nothing to return
end;
initialization
RegisterServiceType(TypeInfo(IDemoService));
RegisterServiceType(TDemoService);
end.
Hosted with Sparkle's Indy-based server, which needs no URL reservation and no administrator rights - handy for a test like this:
program Demo_Server;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
Sparkle.Indy.Server,
XData.Server.Module,
Demo_Service in 'Demo_Service.pas';
const
Port = 2099;
Base_Url = 'http://localhost:2099/demo';
var
LServer: TIndySparkleHTTPServer;
begin
LServer := TIndySparkleHTTPServer.Create(nil);
try
LServer.DefaultPort := Port;
LServer.Dispatcher.AddModule(TXDataServerModule.Create(Base_Url));
LServer.Active := True;
WriteLn('Listening on ', Base_Url);
WriteLn('Press Enter to stop');
ReadLn;
finally
LServer.Free;
end;
end.
| Call | Result |
|---|---|
Server_Time (no attribute) with POST | 200, {"value": "2026-10-21 02:40:25"} |
Server_Time with GET | 405 Method Not Allowed |
Square ([HttpGet]) as GET /DemoService/Square?N=12 | 200, {"value": 144} |
Square with POST | 405 |
Place_Order with body {"Product":"Bolt","Quantity":7} | 200, {"value": 70} |
Place_Order with body {"Order":{"Product":"Bolt","Quantity":7}} | 500, InvalidJsonProperty |
Place_Order with GET | 405 |
Log_Out (a procedure) | 204 No Content, empty body |
Server_Time with POST and no body at all | 200 |
What that shows:
$model lists it with "HttpMethod": "POST". GET has to be asked for
with [HttpGet].Square?N=12. POST parameters go in
a JSON body, named after the parameters.Place_Order takes one
object, so the body is that object's properties - wrapping it in the parameter name fails.{} also works.All of these must be true:
Typical good uses: a lookup list, the server's version, a dashboard figure for a date.
TStream.Since POST is XData's default, leaving the attribute off is the safe choice. Many projects
still write [HttpPost] on every method so the intent is visible.
The GET reply from the test server carried no Cache-Control header. Browsers do
not normally cache a reply without one, but shared proxies and some corporate gateways can.
If a GET returns anything specific to the user, say so explicitly. The test service does it in
Square:
TXDataOperationContext.Current.Response.Headers.SetValue('Cache-Control', 'no-store');
The reply then included Cache-Control: no-store.
How much breaks depends on how each client calls the server.
TXDataWebClient.RawInvoke reads the method from the
server's model when it connects - in the XData web client source the request is built with
Req.Method := Action.HttpMethod. Change the attribute, deploy the server, and the
next session follows. A browser tab opened before the change still has the old model and gets
405 until it reloads.TXDataClient call through the same interface
declaration, so they follow the attribute once they are rebuilt with the new interface.TRESTClient,
THTTPClient, a PHP page, a monitoring tool, a mobile app - has the method written
into its code and gets 405 until it is changed. These are easy to forget because they are not
in the same project as the server.For an operation that other programs call, the safe route is to add the new version alongside the old one, move the callers over, then remove the old one:
[HttpGet]
function Refresh_Session(const Session_Ref: Int64): Boolean; // old, to be removed
[HttpPost]
function Refresh_Session_V2(const Session_Ref: Int64): Boolean; // new
Put the work in one private method that both call. Do not make the old operation call the new one through the service interface - when the old one is removed, it is easy to delete the real code with it, or leave callers pointing at a version that no longer exists. Once every caller has moved, remove the old operation and, if you want the plain name back, rename the new one in a second release.
[HttpGet]; the other method
gets 405.Cache-Control: no-store to GET replies that are specific to the user.
Please chip in if helpful