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. TheResult
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 classicprocedure
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 newMVCResponseBuilder
which allows with a fluent interface to easily createIMVCResponse
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.
- 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
-
β‘ 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 forTMVCWebRequest
andTMVCWebResponse
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 theactiverecord_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 methodSelectByNamedQuery<T>
orSelectByNamedQuery
. 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>
(howeverObjectDict
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 inHTTPStatusCode
. This is a potential breaking change but is very simple to fix, just renameHTTPErrorCode
toHTTPStatusCode
. -
β‘ Removed
statuscode
,reasonstring
and all the fields with a default value from exception’s JSON rendering. All the high-level rendering methods will emit standardReasonString
.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