Pacific Blue Software Logo

GET or POST for TMS XData Service Operations

GET or POST for TMS XData Service Operations

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.

The Test Service

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.

What Happened

CallResult
Server_Time (no attribute) with POST200, {"value": "2026-10-21 02:40:25"}
Server_Time with GET405 Method Not Allowed
Square ([HttpGet]) as GET /DemoService/Square?N=12200, {"value": 144}
Square with POST405
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 GET405
Log_Out (a procedure)204 No Content, empty body
Server_Time with POST and no body at all200

What that shows:

When GET Is Right

All of these must be true:

Typical good uses: a lookup list, the server's version, a dashboard figure for a date.

When POST Is Right

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.

Caching

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.

Changing the Method Later

How much breaks depends on how each client calls 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.

Summary


PBS Delphi - HTTP Methods in TMS XData


Back to Articles for Developers
Back to Articles on Delphi
Implement Virtual Tables in TMS WebCore and XData
How to Send Int64 Values from Delphi to JavaScript
How to Stop the Red Error Panel in TMS WEB Core

If you found this useful, then please consider chipping in.
Chip In with PayPal
QR Code to chip in Please chip in if helpful