Delphi DA REST server versioning with code-first server

Hi,

I am working on a REST implementation using Data Abstract in Delphi 10.4. One of the things I’m trying to plan is allowing a new version of various endpoints to exist within the same application. I am using code first, and I’m curious if there is a best practice or a built-in versioning feature that handles allowing multiple versions of the same endpoint within the same application?

The current design puts the version into the URL:

if I have a change to foo, I’d like to have a new version for both endpoints, and bar would inherit the behavior from v1:

I can subclass the existing Service to a new class and put the new methods there, with the HttpApiPath updated to ‘v2’. I have this working, and seems like it would work well when there is a breaking change and a method needs to be updated/changed.

TVersion1Test = Class(TDataAbstractService)
    [ROServiceMethod]
    [ROCustom('HttpApiMethod','POST')]
    [ROCustom('HttpApiPath','v1/foo')]
    procedure foo; virtual;

    [ROServiceMethod]
    [ROCustom('HttpApiMethod','POST')]
    [ROCustom('HttpApiPath','v1/bar')]
    procedure bar; virtual;

TVersion2Test = Class(TVersion1Test)
    [ROServiceMethod]
    [ROCustom('HttpApiMethod','POST')]
    [ROCustom('HttpApiPath','v2/foo')]
    procedure foo; override;  // will impelement new stuff

    [ROServiceMethod]
    [ROCustom('HttpApiMethod','POST')]
    [ROCustom('HttpApiPath','v2/bar')]
    procedure bar; override;  // will call inherited

If we assume that procedure Bar() is the same in TVersion1Test and TVersion2Test, is there a way to register the new HttpApiPath for ‘v2/bar’ with the existing TVersion1Test.Bar() method? This way, I would only need to declare methods that actually change between versions.

I was wondering if I could re-declare the HttpApiPath on the existing class, but this seems to replace the existing HttpApiPath, which isn’t surprising:

TVersion1Test = Class(TDataAbstractService)
    [ROServiceMethod]
    [ROCustom('HttpApiMethod','POST')]
    [ROCustom('HttpApiPath','v1/foo')]
    procedure foo; virtual;

    [ROServiceMethod]
    [ROCustom('HttpApiMethod','POST')]
    [ROCustom('HttpApiPath','v1/bar')]  // no longer works
    [ROCustom('HttpApiPath','v2/bar')]  // works
    procedure bar; virtual;

TVersion2Test = Class(TVersion1Test)
    [ROServiceMethod]
    [ROCustom('HttpApiMethod','POST')]
    [ROCustom('HttpApiPath','v2/foo')]
    procedure foo; override;  // will impelement new stuff

Thanks for any input,
Leo

Hi,

if you dislike this workaround:

procedure bar; override;  // will call inherited

I can suggest other one. check this snippet: Using OnCustomResponseEvent in a ROSDK Server.

How it should work:

  • you have ROHttpApiDispatcher1. it handles v1 path.
  • you have ROHttpApiDispatcher2. it handles v2 path but his initialization is non-standard:
  Server.Active := True;

  // these lines are needed!
  // as a result, ROHttpApiDispatcher2 can handle `/v2` 
  // but `Server` will think that `/v2` is unhandled path
  ROHttpApiDispatcher2.Server := Server; 
  ROHttpApiDispatcher2.Activate;
  ROHttpApiDispatcher2.Server := nil;
  • you have OnCustomResponseEvent like
procedure TServerMainForm.ROServerCustomResponseEvent(
  const aTransport: IROHTTPTransport; const aRequestStream,
  aResponseStream: TStream; const aResponse: IROHTTPResponse;
  var aHandled: Boolean);

  // your method for detection of redirects
  // it can be hardcoded like my one
  // or you can read from global variable, etc
  function _CheckForRedirect(aValue: string): string;
  begin
    if aValue = '/v1/bar' then 
      Result := '/v2/bar' 
    else 
      Result := aValue;
  end;
var
   l_path: string;
   l_HttpApiDispatcher: TROHttpApiDispatcher;
begin
  // handle all requests started with '/v2'
  if (Pos('/v2/',aTransport.PathInfo) = 1) or (aTransport.PathInfo = '/v2') then begin    
    l_path := _CheckForRedirect(aTransport.PathInfo); 
    aTransport.PathInfo := l_path;
    if (Pos('/v2/', l_path) = 1) or (l_path = '/v2') then
      l_HttpApiDispatcher := ROHttpApiDispatcher2
    else if (Pos('/v1/', l_path) = 1) or (l_path = '/v1') then
      l_HttpApiDispatcher := ROHttpApiDispatcher1
    else 
      Exit;
    l_HttpApiDispatcher.Process(aTransport, aTransport as IROHTTPRequest, aResponse, aRequestStream, aResponseStream);
    aHandled := True;    
  end;
end;

Thanks for the response, I will give that a try and it looks like what I need