DelphiMVCFramework 3.4.0-neon

πŸ‘‰ DelphiMVCFramework 3.4.0-neon is out!

Tons of new features, some bug fixes and improvements available. Let’s see what’s new!

Yes! Following the promises to have more shorter release cycle for DelphiMVCFramework, after only 5 months of development, more than 40 contributors and lot of commits, DelphiMVCFramework 3.4.0-neon is finally out! It is a very important release which bring some changes that will likely change the way you’ll write your APIs in the future. This is, as usual, the best version… so far. This version is already used in production on some services around the world, so do your test as usual, but I’m quite confident that following the recomendation you find in this article, the migration from the previous version is quite simple.

The latest DelphiMVCFramework version is always available at this link.

Ok, let’s go deep in the what’s new section.

What’s new in DelphiMVCFramework-3.4.0-neon

  • ⚑ NEW! Functional Actions

    In addition to the classic procedure based actions, now it’s possibile to use functions as actions. The Result variable is automatically rendered and, if it is an object, its memory is freed.

    Here’s an example of a controller which uses function actions instead of classic procedure actions. Check sample function_actions_showcase.dproj for more info.


type
  [MVCNameCase(ncCamelCase)]
  TPersonRec = record
    FirstName, LastName: String;
    Age: Integer;
    class function Create: TPersonRec; static;
  end;

  [MVCNameCase(ncCamelCase)]
  TPerson = class
  private
    fAge: Integer;
    fFirstName, fLastName: String;
  public
    property FirstName: String read fFirstName write fFirstName;
    property LastName: String read fLastName write fLastName;
    property Age: Integer read fAge write fAge;
    constructor Create(FirstName, LastName: String; Age: Integer);
  end;

  [MVCPath('/api')]
  TMyController = class(TMVCController)
  public
    { actions returning a simple type }
    [MVCPath('/sumsasinteger/($A)/($B)')]
    function GetSum(const A, B: Integer): Integer;
    [MVCPath('/sumsasfloat/($A)/($B)')]
    function GetSumAsFloat(const A, B: Extended): Extended;

    { actions returning records }
    [MVCPath('/records/single')]
    function GetSingleRecord: TPersonRec;
    [MVCPath('/records/multiple')]
    function GetMultipleRecords: TArray;

    { actions returning objects }
    [MVCPath('/objects/single')]
    function GetSingleObject: TPerson;
    [MVCPath('/objects/multiple')]
    function GetMultipleObjects: TObjectList;
    [MVCPath('/objects/jsonobject')]
    function GetJSONObject: TJSONObject;
    [MVCPath('/objects/jsonarray')]
    function GetJSONArray: TJsonArray;

    { actions returning datasets }
    [MVCPath('/datasets/single')]
    function GetSingleDataSet: TDataSet;
    [MVCPath('/datasets/multiple')]
    function GetMultipleDataSet: TEnumerable;
    [MVCPath('/datasets/multiple2')]
    function GetMultipleDataSet2: IMVCObjectDictionary;

    { customize response headers }
    [MVCPath('/headers')]
    function GetWithCustomHeaders: TObjectList;

    { using IMVCResponse }
    [MVCPath('/mvcresponse/message')]
    function GetMVCResponseSimple: IMVCResponse;
    [MVCPath('/mvcresponse/data')]
    function GetMVCResponseWithData: IMVCResponse;
    [MVCPath('/mvcresponse/data/message')]
    function GetMVCResponseWithDataAndMessage: IMVCResponse;
    [MVCPath('/mvcresponse/json')]
    function GetMVCResponseWithJSON: IMVCResponse;
    [MVCPath('/mvcresponse/list')]
    function GetMVCResponseWithObjectList: IMVCResponse;
    [MVCPath('/mvcresponse/dictionary')]
    function GetMVCResponseWithObjectDictionary: IMVCResponse;
    [MVCPath('/mvcresponse/error')]
    function GetMVCErrorResponse: IMVCResponse;
    [MVCPath('/mvcresponse/message/builder/headers')]
    function GetMVCResponseSimpleBuilderWithHeaders: IMVCResponse;
  end;


  • ⚑ Improved TMVCResponse type to better suits the new functional actions. Is available a brand new MVCResponseBuilder which allows with a fluent interface to easily create IMVCResponse instances (see the following section).

    TMVCResponse can be used with “message based” responses and also with “data based” responses (with single object, with a list of objects or with a dictionary of objects).

    Message based responses


function TMyController.GetMVCResponse: IMVCResponse;
begin
  Result := MVCResponseBuilder
    .StatusCode(HTTP_STATUS.OK)
    .Body('My Message')
    .Build;
end;

Produces


{
  "message":"My Message"
}

Data based response with single object


function TMyController.GetMVCResponseWithData: IMVCResponse;
begin
  Result := MVCResponseBuilder
    .StatusCode(HTTP_STATUS.OK)
    .Body(TPerson.Create('Daniele','Teti', 99))
    .Build;
end;

Produces


{
  "data": {
    "firstName": "Daniele",
    "lastName": "Teti",
    "age": 99
  }
}

Data based response with list of objects


function TMyController.GetMVCResponse3: IMVCResponse;
begin
  Result := MVCResponseBuilder
    .StatusCode(HTTP_STATUS.OK)
    .Body(TObjectList.Create([
      TPerson.Create('Daniele','Teti', 99),
      TPerson.Create('Peter','Parker', 25),
      TPerson.Create('Bruce','Banner', 45)
    ])
  ).Build;
end;

Produces


{
  "data": [
    {
      "firstName": "Daniele",
      "lastName": "Teti",
      "age": 99
    },
    {
      "firstName": "Peter",
      "lastName": "Parker",
      "age": 25
    },
    {
      "firstName": "Bruce",
      "lastName": "Banner",
      "age": 45
    }
  ]
}

Data dictionary based response with IMVCObjectDictionary


function TMyController.GetMVCResponseWithObjectDictionary: IMVCResponse;
begin
  Result := MVCResponseBuilder
    .StatusCode(HTTP_STATUS.OK)
    .Body(ObjectDict()
      .Add('customers', TObjectList.Create([
                      TPerson.Create('Daniele','Teti', 99),
                      TPerson.Create('Peter','Parker', 25),
                      TPerson.Create('Bruce','Banner', 45)
                    ])
      )
      .Add('employees', TObjectList.Create([
                      TPerson.Create('Daniele','Teti', 99),
                      TPerson.Create('Peter','Parker', 25),
                      TPerson.Create('Bruce','Banner', 45)
                    ])
      )
  )
  .Build;
end;

Produces


{
  "employees": [
    {
      "firstName": "Daniele",
      "lastName": "Teti",
      "age": 99
    },
    {
      "firstName": "Peter",
      "lastName": "Parker",
      "age": 25
    },
    {
      "firstName": "Bruce",
      "lastName": "Banner",
      "age": 45
    }
  ],
  "customers": [
    {
      "firstName": "Daniele",
      "lastName": "Teti",
      "age": 99
    },
    {
      "firstName": "Peter",
      "lastName": "Parker",
      "age": 25
    },
    {
      "firstName": "Bruce",
      "lastName": "Banner",
      "age": 45
    }
  ]
}

  • ⚑ NEW! Support for dotEnv configuration files

    • dotEnv is very popular among any programming languages, it is simple to use an integrates well in clound, side-by-side and continuos deployment scenarios.
    • DelphiMVCFramework supports multiline keys and key expansions too.
    • Demystifying the Usage of dotEnv in dmvcframework is a very practical article about dotEnv in DelphiMVCFramework - available for Patron supporters.
    • dotEnv_showcase is the official sample available in the repo to show how to use dotEnv.
    • dotEnv support provide “profiles” too and allows for a hierarchical configuration which is very useful to simplify your system configuration is complex environments.
    • While dotEnv support is integrated in DelphiMVCFramework it doesn’t require it, so you can use dotEnv support in your applications (VCL, FireMonkey, etc) and not only in DelphiMVCFramework projects.
  • ⚑ NEW! IDE Wizard updated to be dotEnv aware.

    • By default the generated code uses dotEnv configuration. It means that you project alrady has a built-in configuration sustem and already use it, if available. For examplem if you want to change the TCP port where your service listen (using the built-in webserver) you can just create a .env file in the same executable folder and define the following key: env dmvc.server.port = 9090 You can also add all the configuration values your project needs.
  • ⚑ NEW! Added MSHeap memory manager for Win32 and Win64. If you are on MSWindows give a try to this MSHeap wrapper. It is a very simple change to your code which allows to gain more speed at no cost. Just add MSHeap to your .dpr project file. Thank you to RDP1974 for its support.

  • ⚑ NEW! HTMX server side support through unit samples\htmx\MVCFramework.HTMX.pas and the relative sample. This unit provides class helper for TMVCWebRequest and TMVCWebResponse classes to easily work with HTMX. If you want to use this unit just download the samples and add it to your project or put $(DMVCHOME)\samples\htmx in your library path. Thanks to David Moorhouse to have developed these helpers.

  • ⚑ NEW! Added “Load Style” methods to TMVCActiveRecord.

    TMVCActiveRecord support “Factory Style” and “Load Style” methods when loads data from database. Using “Factory Style” methods (availables from the first version) the result list containing data is returned by the loader method (as shown in this piece of code from the activerecord_showcase sample).


Log('>> RQL Query (2) - ' + cRQL2);
lCustList := TMVCActiveRecord.SelectRQL(cRQL2, 20);  //<< Factory Style
try
  Log(lCustList.Count.ToString + ' record/s found');
  for lCustomer in lCustList do
  begin
    Log(Format('%5s - %s (%s)', [lCustomer.Code.ValueOrDefault,
      lCustomer.CompanyName.ValueOrDefault, lCustomer.City]));
  end;
finally
  lCustList.Free;
end;

For some scenarios would be useful to have also “Load Style” methods where a list already instantiated is just filled by the loader method (so not instantiated internally).


Log('>> RQL Query (2) - ' + cRQL2);
lCustList := TObjectList.Create;
try
  lRecCount := TMVCActiveRecord
    .SelectRQL(cRQL2, 20, lCustList); //New in 3.4.0-neon, Load Style
  Log(lRecCount.ToString + ' record/s found');
  for lCustomer in lCustList do
  begin
    Log(Format('%5s - %s (%s)', [lCustomer.Code.ValueOrDefault,
      lCustomer.CompanyName.ValueOrDefault, lCustomer.City]));
  end;
finally
  lCustList.Free;
end;

  • ⚑ New! SQL and RQL Named Queries support for TMVCActiveRecord.

    MVCNamedSQLQuery allows to define a “named query” which is, well, a SQL query with a name. Then such query can be used by the method SelectByNamedQuery<T> or SelectByNamedQuery. Moreover in the attribute it is possible to define on which backend engine that query is usable. In this way you can define optimized query for each supported DMBS you need because the framework will pick the correct one automatically. Check the example below.


type
  [MVCTable('customers')]
  [MVCNamedSQLQuery('RatingLessThanPar', 
    'select * from customers where rating < ? order by code, city desc')]
  [MVCNamedSQLQuery('RatingEqualsToPar', 
    'select /*firebird*/ * from customers where rating = ? order by code, city desc',
    TMVCActiveRecordBackEnd.FirebirdSQL)]
  [MVCNamedSQLQuery('RatingEqualsToPar', 
    'select /*postgres*/ * from customers where rating = ? order by code, city desc',
    TMVCActiveRecordBackEnd.PostgreSQL)]
  [MVCNamedSQLQuery('RatingEqualsToPar', 
    'select /*all*/ * from customers where rating = ? order by code, city desc')]
  TCustomer = class(TCustomEntity)
  private
  // usual field declaration
  end;
  
  //** then in the code
  
  Log('** Named SQL Query');
  Log('QuerySQL: RatingLessThanPar');
  var lCustomers := TMVCActiveRecord.SelectByNamedQuery(
      'RatingLessThanPar', [4], [ftInteger]);
  try
    for var lCustomer in lCustomers do
    begin
      Log(Format('%4d - %8.5s - %s', [
          lCustomer.ID.ValueOrDefault, 
          lCustomer.Code.ValueOrDefault,
          lCustomer.CompanyName.ValueOrDefault]));
    end;
  finally
    lCustomers.Free;
  end;

  Log('QuerySQL: RatingEqualsToPar');
  lCustomers := TMVCActiveRecord.SelectByNamedQuery(
      'RatingEqualsToPar', [3], [ftInteger]);
  try
    for var lCustomer in lCustomers do
    begin
      Log(Format('%4d - %8.5s - %s', [
        lCustomer.ID.ValueOrDefault, 
        lCustomer.Code.ValueOrDefault,
        lCustomer.CompanyName.ValueOrDefault]));
    end;
  finally
    lCustomers.Free;
  end;

The same approach is available for RQL query, which can be used also for Count and Delete operations but doesnt allows to specify the backend (because RQL has an actual compiler to adapt the generated SQL to each RDBMS)


type
  [MVCTable('customers')]
  [MVCNamedSQLQuery('RatingLessThanPar', 
    'select * from customers where rating < ? order by code, city desc')]
  [MVCNamedSQLQuery('RatingEqualsToPar', 
    'select /*firebird*/ * from customers where rating = ? order by code, city desc', 
    TMVCActiveRecordBackEnd.FirebirdSQL)]
  [MVCNamedSQLQuery('RatingEqualsToPar', 
    'select /*postgres*/ * from customers where rating = ? order by code, city desc', 
    TMVCActiveRecordBackEnd.PostgreSQL)]
  [MVCNamedSQLQuery('RatingEqualsToPar', 
    'select /*all*/ * from customers where rating = ? order by code, city desc')]
  [MVCNamedRQLQuery('RatingLessThanPar', 'lt(rating,%d);sort(+code,-city)')]
  [MVCNamedRQLQuery('RatingEqualsToPar', 'eq(rating,%d);sort(+code,-city)')]
  TCustomer = class(TCustomEntity)
  private
  // usual field declaration
  end;
  
  //** then in the code
  
  Log('** Named RQL Query');
  Log('QueryRQL: RatingLessThanPar');
  lCustomers := TMVCActiveRecord.SelectRQLByNamedQuery(
      'RatingLessThanPar', [4], 1000);
  try
    for var lCustomer in lCustomers do
    begin
      Log(Format('%4d - %8.5s - %s', [
          lCustomer.ID.ValueOrDefault, 
          lCustomer.Code.ValueOrDefault,
          lCustomer.CompanyName.ValueOrDefault]));
    end;
  finally
    lCustomers.Free;
  end;

  Log('QueryRQL: RatingEqualsToPar');
  lCustomers := TMVCActiveRecord.SelectRQLByNamedQuery(
      'RatingEqualsToPar', [3], 1000);
  try
    for var lCustomer in lCustomers do
    begin
      Log(Format('%4d - %8.5s - %s', [
          lCustomer.ID.ValueOrDefault, 
          lCustomer.Code.ValueOrDefault,
          lCustomer.CompanyName.ValueOrDefault]));
    end;
  finally
    lCustomers.Free;
  end;
  

Now, having SQL and RQL named queries, it is possibile to have an entity which is not mapped on a specific table but loaded only by named queries. Such kind of entities must be declared using [MVCEntityActions(eaRetrieve)].


type
  [MVCEntityActions([eaRetrieve])]  // <-- Required if "MVCTable" is not present.
  [MVCNamedSQLQuery('CustomersInTheSameCity',
    'SELECT c.id, c.DESCRIPTION, c.city, c.code, c.rating, (SELECT count(*) - 1 FROM customers c2 WHERE c2.CITY = c.CITY) customers_in_the_same_city ' +
    'FROM CUSTOMERS c WHERE city IS NOT NULL AND city <> '''' ORDER BY customers_in_the_same_city')]
  TCustomerStats = class(TCustomEntity) {not mapped on an actual table or view}
  private
    [MVCTableField('id', [foPrimaryKey, foAutoGenerated])]
    fID: NullableInt64;
    [MVCTableField('code')]
    fCode: NullableString;
    [MVCTableField('description')]
    fCompanyName: NullableString;
    [MVCTableField('city')]
    fCity: string;
    [MVCTableField('rating')]
    fRating: NullableInt32;
    [MVCTableField('customers_in_the_same_city')]
    fCustomersInTheSameCity: Int32;
  public
    property ID: NullableInt64 read fID write fID;
    property Code: NullableString read fCode write fCode;
    property CompanyName: NullableString 
      read fCompanyName write fCompanyName;
    property City: string read fCity write fCity;
    property Rating: NullableInt32 read fRating write fRating;
    property CustomersInTheSameCity: Int32 
      read fCustomersInTheSameCity write fCustomersInTheSameCity;
  end;


//** then in the code

procedure TMainForm.btnVirtualEntitiesClick(Sender: TObject);
begin
  var lCustStats := TMVCActiveRecord.SelectByNamedQuery(
      'CustomersInTheSameCity', [], []);
  try
    for var lCustomer in lCustStats do
    begin
      Log(Format('%4d - %8.5s - %s - (%d other customers in the same city)', [
        lCustomer.ID.ValueOrDefault,
        lCustomer.Code.ValueOrDefault,
        lCustomer.CompanyName.ValueOrDefault,
        lCustomer.CustomersInTheSameCity
        ]));
    end;
  finally
    lCustStats.Free;
  end;
end;

Here’s all the new methods available for Named Queries


class function SelectByNamedQuery(
  const QueryName: String;
  const Params: array of Variant;
  const ParamTypes: array of TFieldType;
  const Options: TMVCActiveRecordLoadOptions = []): TObjectList; overload;
class function SelectByNamedQuery(
  const MVCActiveRecordClass: TMVCActiveRecordClass;
  const QueryName: String;
  const Params: array of Variant;
  const ParamTypes: array of TFieldType;
  const Options: TMVCActiveRecordLoadOptions = []): TMVCActiveRecordList; overload;
class function SelectRQLByNamedQuery(
  const QueryName: String;
  const Params: array of const;
  const MaxRecordCount: Integer): TObjectList; overload;
class function SelectRQLByNamedQuery(
  const MVCActiveRecordClass: TMVCActiveRecordClass;
  const QueryName: String;
  const Params: array of const;
  const MaxRecordCount: Integer): TMVCActiveRecordList; overload;
class function DeleteRQLByNamedQuery(
  const QueryName: String;
  const Params: array of const): Int64;
class function CountRQLByNamedQuery(
  const QueryName: string;
  const Params: array of const): Int64;

  • ⚑ Improved Better error message in case of serialization of TArray<TObject>
  • ⚑ Improved Better CORS handling - Issue 679 Thanks to David Moorhouse
  • ⚑ Improved Better serialization of TObjectList<TDataSet> (however ObjectDict is still the preferred way to serialize multiple datasets).
  • ⚑ NEW! Static method for easier cloning of FireDAC dataset into TFDMemTable.

  class function CloneFrom(const FDDataSet: TFDDataSet): TFDMemTable

Check sample function_actions_showcase.dproj for more info.

  • ⚑ Improved Property EMVCException.HTTPErrorCode has been renamed in HTTPStatusCode. This is a potential breaking change but is very simple to fix, just rename HTTPErrorCode to HTTPStatusCode.

  • ⚑ Removed statuscode, reasonstring and all the fields with a default value from exception’s JSON rendering. All the high-level rendering methods will emit standard ReasonString.

    Before


{
    "apperrorcode": 0,
    "statuscode": 404,
    "reasonstring": "Not Found"
    "classname": "EMVCException",
    "data": null,
    "detailedmessage": "",
    "items": [],
    "message": "[EMVCException] Not Found"
}
  

Now (cleaner and shorter)


{
    "classname": "EMVCException",
    "message": "Not Found"
}

Bug Fixes in DelphiMVCFramework-3.4.0-neon

  • 🐞 FIX Issue 664 Thanks to MPannier
  • 🐞 FIX Issue 667
  • 🐞 FIX Issue 680
  • 🐞 FIX Issue 682 Thanks to wuhao13
  • 🐞 FIX Wrong comparison in checks for ro/RW/PK fields in TMVCActiveRecord that could arise is some extreme cases.
  • 🐞 FIX wrong default initialization for JWT - Thanks to Flavio Basile

DelphiMVCFramework Book

While all these new features can radically change and simplify the way you’ll write your next APIs, the contents in the Official Guide is still largely relevant and all the concepts explained there are still valid. To avoid to reinvent-the-weel, and to follow industry best practices, I suggest to all the serious DMVCFramework users to read DelphiMVCFramework: the official guide book that will cover from the basic utilization to the advanced scenarios with a lot real-world samples and how to sections. More info about the book and its translations here.

Official presentation at ITDevCon2023

As a nice tradition, the latest version of DMVCFramework is always presented to the “next” ITDevCon. This year ITDevCon will be in Rome, October 26th and 27th 2023. Take your seat!

Enjoy!

– Daniele Teti

Comments

comments powered by Disqus