DMVCFramework (for short) is a popular and powerful framework for web solution in Delphi. Supports RESTful and JSON-RPC APIs development.
Table of Contents
DMVCFramework is a very popular Delphi framework which provides an easy to use, scalable, flexible RESTful, JSON-RPC and ActiveRecord framework for Delphi developers. DMVCFramework is the most popular Delphi project on GitHub and compiles for Windows (32 and 64bit) and Linux (64bit). DMVCFramework services can be compiled as console application, Windows Service, Linux daemon, Apache module (Windows and Linux) and IIS ISAPI (Windows).
DMVCFramework works with Delphi 10.4 Sydney, Delphi 10.3 Rio, Delphi 10.2 Tokyo, Delphi 10.1 Berlin, Delphi 10 Seattle.
DMVCFramwork is simple to use, has a lot of examples, is documented and there are a lot of tutorials available.
DMVCFramework is very well documented and the book DMVCFramework - the official guide is available to fastly get a solid knowledge.
Project Roadmap is always public
HTTP Basic Authentication
JWT Authentication
Custom Authentication
CORS support
[email protected]for a date and a place)
/system/describeserver.info
If you are not involved in development or testing, do not clone the repo! Use the Github release!
The last stable version is dmvcframework-3.2.1-carbon available here 📥. Just download latest release as a zip file and you are ok. The samples are availables as separate zip file downloadable from the same page where you download the release.
The official guide for DMVCFramework is available. DMVCFramework has a lot functionalities and can really help your business. However many developers don't use it at its full potential. Why don't get more when is easily available? The DMVCFramework lead developer and project coordinator Daniele Teti, wrote the official guide for this great framework.
Buy your copy and improve your DMVCFramework knowledge now!
While a huge work has been done by the author and the reviews to make the book and the examples well written, complete and effective, things can be always improved. For any suggestions, complains or requests there is the official Github book project (https://github.com/danieleteti/dmvcframeworktheofficialguide) where you can fill an issue and get in touch directly with the author.
Given the success of DMVCFramework in the Delphi community, the official DMVCFramework guide has been translated also in the following languages.
Please, if you use DMVCFramework "star" this project in GitHub! It cost nothing to you but helps other developers to reference the code.
Only if you want to participate to the testing phase (which usually contains brand new features but can sometimes be instable) you can get the development version clonig this repo or downloading the master repository zip file. Take in mind that even if development version is usually very stable, it isn't not ready for production utilization.
"DMVCFramework and the Entity utility are fantastic!" -- Roberto
"DMVCFramework is a great framework. It's very intuitive, fast, easy to use, actually there is nothing more to ask for." -- Samir
"Wow! To do that in J2EE it takes 2 days" -- a training participant after a 5 minutes demo.
"I'm starting with the DMVCFramework and I'm finding it fantastic, congratulations for the project!" -- Rafael
"I'm looking at DMVCFramework project in it works great - for my use case scenarios is much better than 'Similar commercial product'." -- Luka
"It's fantastic! Just define your entities and you are up and running in 5 minutes. Nothing comparable on the market." -- Marco
"The best framework for creating web servers with Delphi! It is very easy to create Delphi servers and publish APIs and Rest resources. Congratulations to Daniele Teti and all the staff for the excellent work!" -- Marcos N.
We started the process of migrating our systems to micro services and are loving the DMVCFramework "DMVCFramework is definitely part of our lives right now". -- E. Costa
"Thank you for the great framework! We are very happy with this!" -- Andreas
"Our wishes are coming true" -- one Delphi programmer after a small dmvcframework demo for an IT department of a very important national research institute
This version is the version referenced by the DelphiMVCFramework - The Official Guide book (available in english, portuguese and spanish).
This version introduced new features in many different areas (swagger, server side view, MVCActiveRecord, renders etc.) however there is no a single-big-feature. This version contains also a good number of bugfixes. It's not a critical updated, but this is the best version ever (at least, so far...) and is the suggested version for starting new projects. Enjoy!
New
Context: TWebContextparameter in JSON-RPC Hooks
{ Called before any actual routing } procedure OnBeforeRoutingHook(const Context: TWebContext; const JSON: TJsonObject); { Called after routing and before the actual remote method invocation } procedure OnBeforeCallHook(const Context: TWebContext; const JSON: TJsonObject); { Called after actual remote method invocation, even if the method raised an exception } procedure OnAfterCallHook(const Context: TWebContext; const JSON: TJsonObject);
When a JSON-RPC Request returns a
System.Booleanthe
resultwill be a JSON
trueor
falseand no
1or
0as it was in the
3.2.0-boron.
IMVCJSONRPCExecutor.ExecuteNotificationreturns a
IJSONRPCResponse. In case of error response contains information about the error, in case of successful execution the response is a Null Object.
New React demo (Thanks to Angelo Sobreira da Silva)
Serialization support for
TList,
TList,
TListand for all
TListof simple types.
Added method
MetadataAsJSONObject(FieldNameCase: TMVCNameCase = ncLowerCase): TJSONObject;in
TDataSetHelper. This method returns the dataset field definitions. While this is valid only for Delphi datasets, can be useful to describe a dataset to a Delphi client.
{ "fielddefs": [ { "datatype": 3, "displayname": "ID", "fieldname": "id", "precision": 0, "size": 0 }, { "datatype": 24, "displayname": "CODE", "fieldname": "code", "precision": 0, "size": 5 }, { "datatype": 24, "displayname": "DESCRIPTION", "fieldname": "description", "precision": 0, "size": 200 }, { "datatype": 37, "displayname": "PRICE", "fieldname": "price", "precision": 18, "size": 2 } ] }
The static method
class procedure TFireDACUtils.CreateDatasetFromMetadata( AFDMemTable: TFDMemTable; AMeta: TJSONObject);gets the previous structure and initialize the fields of
AFDMemTablewith it. When a TFDMemTable is initialized using this approach, the data can be directly loaded from a jsonarray of jsonobject with the same field names. WARNING: This mechanism works only for Delphi clients. Check sample
articles_crud_vcl_client_meta.dprojto understand the involved mechanisms.
Added
foReadOnlyand
foWriteOnlyas field options in
MVCTableFieldattribute (used by
TMVCActiveRecord). Currently available field options are:
Now it is possible to declare entities like the followings (or with any other combinations):
[MVCNameCase(ncLowerCase)] [MVCTable('articles')] TArticleWithWriteOnlyFields = class(TCustomEntity) private [MVCTableField('ID', [foPrimaryKey, foAutoGenerated])] fID: NullableInt32; [MVCTableField('description', [foWriteOnly])] fDescription: string; [MVCTableField('price', [foWriteOnly])] fPrice: Integer; public property ID: NullableInt32 read fID write fID; property Description: string read fDescription write fDescription; property Price: Integer read fPrice write fPrice; end;[MVCNameCase(ncLowerCase)] [MVCTable('articles')] TArticleWithReadOnlyFields = class(TCustomEntity) private [MVCTableField('ID', [foPrimaryKey, foReadOnly])] fID: NullableInt32; [MVCTableField('code', [foTransient])] fCode: NullableString; [MVCTableField('description', [foReadOnly])] fDescrizione: string; [MVCTableField('price', [foReadOnly])] fPrice: Currency; public property ID: NullableInt32 read fID write fID; property Code: NullableString read fCode write fCode; property Description: string read fDescription write fDescription; property Price: Currency read fPrice write fPrice; end;
BodyAsand for
BodyAsListOf(Thanks to Raphaël Emourgeon for the
BodyAsListOfimplementation).
procedure TBooksController.CreateBook; var lBook: TBook; begin //this call deserialize a TBook instance //starting from the 'book' node of //the request body lBook := Context.Request.BodyAs('book'); try lBook.Insert; Render201Created('/api/books/' + lBook.ID.ToString); finally lBook.Free; end; end;
procedure TMyBaseEntity.OnBeforeInsert; begin inherited; //regardless the name of the PK field //the following code fills the PK with a GUID //Inheriting the other entities from this, all //will inherit this behavior. SetPK(TValue.From(TGUID.NewGuid.ToString));//if the PK was a simple string, the code //should be like the following //SetPK(TGUID.NewGuid.ToString);
end;
Improved
activerecord_showcasesample.
Improved
TMVCStaticFilesMiddleware. Now is able to correctly serve SPA applications from any subfolder.
Added property
Context.HostingFrameworkType. This property is of type
TMVCHostingFrameworkTypeand can assume one of the following values:
hftIndy(if the service is using the built-in Indy HTTP server) ,
hftApache(if the project is compiled as Apache module) or
hftISAPI(if the project is compiled as ISAPI module).
TMVCNameCasegot the new
ncSnakeCaseamong the possibles casing (thanks to João Antônio Duarte for its work in this area).
Now the
TMVCNameCasedeclaration is the following:
TMVCNameCase = (ncAsIs, ncUpperCase, ncLowerCase, ncCamelCase, ncPascalCase, ncSnakeCase);
Here's how the new
ncSnakeCaseworks
| Original | SnakeCase | | -------- | ---------- | | OneTwo | onetwo | | OneTwo | onetwo | | OneTwo03 | onetwo03 |
Support for Mustache partials (Thanks to David Moorhouse and his work about issue 221). Sample \samples\serversideviews_mustache has been updated to show how to use partials.
Added dynamic properties access to
TMVCActiveRecorddescendants. Indexed property
Attributesis index using the property name and set/get a
TValuerepresenting the property value.
procedure TMainForm.btnAttributesClick(Sender: TObject); var lCustomer: TCustomer; lID: Integer; begin lCustomer := TCustomer.Create; try lCustomer.Attributes['CompanyName'] := 'Google Inc.'; lCustomer.Attributes['City'] := 'Montain View, CA'; lCustomer.Attributes['Note'] := 'Hello there!'; lCustomer.Attributes['Code'] := 'XX123'; lCustomer.Attributes['Rating'] := 3; lCustomer.Insert; lID := lCustomer.ID; finally lCustomer.Free; end;lCustomer := TMVCActiveRecord.GetByPK<tcustomer>(lID); try Assert('Google Inc.' = lCustomer.Attributes['CompanyName'] .AsType<nullablestring>().Value); Assert('Montain View, CA' = lCustomer.Attributes['City'].AsString); Assert('XX123' = lCustomer.Attributes['Code'] .AsType<nullablestring>().Value); Assert('Hello there!' = lCustomer.Attributes['Note'].AsString); lCustomer.Update; finally lCustomer.Free; end;
Breaking Change!
TMVCStaticFileMiddlewarecannot be registered to "/" anymore
Breaking Change!
DocumentRootof
TMVCStaticFileMiddlewaremust be a valid folder. If
DocumentRootdoesn't exist an exception is raised.
Fix for issue 421
Fix for issue 424
Fix for issue436.
Fix for issue432
Fix for issue434 (Thanks to David Moorhouse for his detailed analysis)
Fix for issue221
Fix for issue444
Fix for issue408 a.k.a. docExpansion parameter for Swagger
Check all the issues closed in this release.
TMVCRESTClientimplementation based on *Net components, the previous one was based on INDY Components (thanks to João Antônio Duarte).
MVCJSONRPCAllowGETattribute allows a remote JSON-RPC published object, or a specific method, to be called using GET HTTP Verb as well as POST HTTP Verb. POST is always available, GET is available only if explicitly allowed.
IMVCJSONRPCExecutorallows to specify which HTTP Verb to use when call the server JSON.RPC methods. The default verb can be injected in the constructor and each
ExecuteRequest/
ExecuteNotificationallows to override od adhere to the instance default.
TThreadSafeQueueclass uses a cubic function instead of a linear one to wait in case of very high concurrency. This allows a better resiliency in case of high load.
TMVCLRUCacheimplementation. Very efficient implementation of LRU cache borrowed directly from DMSContainer
TMVCActiveRecordsupports XML field type in PostgreSQL (in addition to JSON and JSONB).
activerecord_showcasesample project.
New! Support for Delphi 10.4 Sydney!
New! Added Nullable support in MVCActiveRecord (nullables defined in
MVCFramework.Nullables.pas)! Check activerecord_showcase sample.
New! Added non autogenerated primary keys in MVCActiveRecord! Check activerecord_showcase sample.
New! Complete support for nullable types in the default serializer (nullables defined in
MVCFramework.Nullables.pas)
New! Added
ncCamelCaseand
ncPascalCaseto the available attribute formatters.
| MVCNameCase | Property/Field Name | Rendered Name | | ------------ | --------------------- | --------------- | | ncUpperCase | CodArticle | CODARTICLE | | ncLowerCase | CodArticle | codarticle | | ncPascalCase | CodArticle | CodArticle | | ncPascalCase | CodArticle | CodArticle | | ncPascalCase | `WITHUNDERSCORES` | WithUnderscores | | ncCamelCase | CodArticle | codArticle | | ncCamelCase | CodArticle | codArticle | | ncCamelCase | `_WITHUNDERSCORES_` | WithUnderscores | | | | |
New! Added Swagger support (thanks to João Antônio Duarte and Geoffrey Smith)
New! Attribute
MVCDoNotDeserialize. If marked with this RTTI attribute, a property or a field is not deserialized and its value remain the same as was before the object deserialization.
New! Added SQLGenerator and RQL compiler for PostgreSQL, SQLite and MSSQLServer (in addition to MySQL, MariaDB, Firebird and Interbase)
New! MVCNameAs attribute got the param
Fixed(default: false). If
Fixedis true, then the name is not processed by the
MVCNameCaseattribute assigned to the owner type.
New! Added support for interfaces serialization - now it is possible to serialize Spring4D collections (thanks to João Antônio Duarte)
New! Added support for rendering Spring4D Nullable Types - (thanks to João Antônio Duarte)
New! Added
OnRouterLogevent to log custom information for each request (thanks to Andrea Ciotti for the first implementation and its PR)
New! Optionally load system controllers (those who provide
/describeserver.info,
/describeplatform.infoand
/serverconfig.infosystem actions) setting
Config[TMVCConfigKey.LoadSystemControllers] := 'false';in the configuration block.
Improved! Now the router consider
Accept:*/*compatible for every
MVCProducesvalues.
Improved! Greatly improved support for HATEOAS in renders. Check
TRenderSampleController.GetPeople_AsObjectList_HATEOSand all the others actions end with
HATEOSin
renders.dprojsample).
//Now is really easy to add "links" property automatically for each collection element while rendering Render(People, True, procedure(const APerson: TPerson; const Links: IMVCLinks) begin Links.AddRefLink .Add(HATEOAS.HREF, '/people/' + APerson.ID.ToString) .Add(HATEOAS.REL, 'self') .Add(HATEOAS._TYPE, 'application/json') .Add('title', 'Details for ' + APerson.FullName); Links.AddRefLink .Add(HATEOAS.HREF, '/people') .Add(HATEOAS.REL, 'people') .Add(HATEOAS._TYPE, 'application/json'); end);//Datasets have a similar anon method to do the same thing Render(lDM.qryCustomers, False, procedure(const DS: TDataset; const Links: IMVCLinks) begin Links.AddRefLink .Add(HATEOAS.HREF, '/customers/' + DS.FieldByName('cust_no').AsString) .Add(HATEOAS.REL, 'self') .Add(HATEOAS._TYPE, 'application/json'); Links.AddRefLink .Add(HATEOAS.HREF, '/customers/' + DS.FieldByName('cust_no').AsString + '/orders') .Add(HATEOAS.REL, 'orders') .Add(HATEOAS._TYPE, 'application/json'); end);
//Single object rendering allows HATEOAS too! Render(lPerson, False, procedure(const AObject: TObject; const Links: IMVCLinks) begin Links.AddRefLink .Add(HATEOAS.HREF, '/people/' + TPerson(AObject).ID.ToString) .Add(HATEOAS.REL, 'self') .Add(HATEOAS._TYPE, TMVCMediaType.APPLICATION_JSON); Links.AddRefLink .Add(HATEOAS.HREF, '/people') .Add(HATEOAS.REL, 'people') .Add(HATEOAS._TYPE, TMVCMediaType.APPLICATION_JSON); end);
packagesfolder)
TMVCActiveRecord.Countmethod (e.g.
TMVCActiveRecord.Count(TCustomer)returns the number of records for the entity mapped by the class
TCustomer)
TMVCACtiveRecord.GetByPKraises an exception by default if the record is not found - optionally can returns
nilusing new parameter
RaiseExceptionIfNotFound
containsclause has been added in the RQL compiler for Firebird and Interbase
TMVCAnalyticsMiddlewareto do automatic analytics on the API (generates a CSV file). Based on an idea by Nirav Kaku (https://www.facebook.com/nirav.kaku). Check the sample in
\samples\middleware_analytics\
TMVCActiveRecord.DeleteAlldeletes all the records from a table
TMVCActiveRecord.DeleteRQLdeletes records using an
RQLexpression as
whereclause.
TMVCActiveRecord.Storewhich automatically executes Insert or Update considering primary key value.
TMVCActiveRecordallows to use table name and field name with spaces (currently supported only by the PostgreSQL compiler).
MVCActiveRecordand RQL (thanks to one of the biggest Delphi based company in Italy which heavily uses DMVCFramework and DMSContainer).
MVCActiveRecordand RQL, so that
MVCActiveRecordcan be used also for Delphi mobile projects!
JsonDataObjects.TJSONObjectwithout using
stringas carrier of JSON.
ActiveRecordShowCasesample is much better now.
ActiveRecordmethods which retrieve records can now specify the data type of each parameter (using Delphi's
TFieldTypeenumeration).
TMVCEngineis compliant with the default response content-type (usually it did would reply using
text/plain).
LogD(lMyObject)to get logged
lMyObjectas JSON (custom type serializers not supported in log).
StrDict(array of string, array of string)function allows to render a dictionary of strings in a really simple way. See the following action sample.
procedure TMy.GetPeople(const Value: Integer); begin if Value mod 2 <> 0 then begin raise EMVCException.Create(HTTP_STATUS.NotAcceptable, 'We don''t like odd numbers'); end; Render( StrDict( ['id', 'message'], ['123', 'We like even numbers, thank you for your ' + Value.ToString] )); end;
application/json.
TMVCListener(Thanks to Sven Harazim).
inoperator in RQL parser (Thank you to João Antônio Duarte for his initial work on this).
TMVCActiveRecord.Count(RQL)to count record based on RQL criteria.
TMVCActiveRecordcan handle non autogenerated primary key.
X-HTTP-Method-Overrideto work behind corporate firewalls.
procedure TDataSetHelper.LoadJSONArrayFromJSONObjectProperty(const AJSONObjectString: string; const aPropertyName: String);
HTTP_STATUSto better describe the http status response.
CreateInsertif not autogenerated.
TArray,
TArrayand
TArrayin default JSON serializer (Thank you Pedro Oliveira)
OnBeforeDispatch(Thanks to Spinettaro)
StrToJSONObjectfunction to safely parse a string into a JSON object.
TDataSetdescendants serialization in
TMVCJsonDataObjectsSerializer.
procedure TMainForm.btnDataSetToJSONArrayClick(Sender: TObject); var lSer: TMVCJsonDataObjectsSerializer; lJArray: TJSONArray; begin FDQuery1.Open(); lSer := TMVCJsonDataObjectsSerializer.Create; try lJArray := TJSONArray.Create; try lSer.DataSetToJsonArray(FDQuery1, lJArray, TMVCNameCase.ncLowerCase, [], procedure(const aField: TField; const aJsonObject: TJSONObject; var Handled: Boolean) begin if SameText(aField.FieldName, 'created_at') then begin aJsonObject.S['year_and_month'] := FormatDateTime('yyyy-mm', TDateTimeField(aField).Value); Handled := True; end; end); //The json objects will not contains "created_at" anymore, but only "year_and_month". Memo1.Lines.Text := lJArray.ToJSON(false); finally lJArray.Free; end; finally lSer.Free; end; end;
New! Shortcut render' methods which simplify RESTful API development
procedure Render201Created(const Location: String = ''; const Reason: String = 'Created'); virtual;
procedure Render202Accepted(const HREF: String; const ID: String; const Reason: String = 'Accepted'); virtual;
procedure Render204NoContent(const Reason: String = 'No Content'); virtual;
Added de/serializing iterables (e.g. generic lists) support without
MVCListOfattribute (Thank you to João Antônio Duarte).
It is now possible to deserialize a generic class like this:
TGenericEntity = class private FCode: Integer; FItems: TObjectList; FDescription: string; public constructor Create; destructor Destroy; override; property Code: Integer read FCode write FCode; property Description: string read FDescription write FDescription; // MVCListOf(T) read FItems write FItems; end;
Before it was not possible because you should add the
MVCListOfattribute to the
TObjectListtype property.
New! Added serialization support for (thanks to dockerandy for his initial work)
TArray
TArray
TArray
TArray
New! The MVCAREntitiesGenerator can optionally register all the generated entities also in the
ActiveRecordMappingRegistry(Thanks to Fabrizio Bitti from bit Time Software)
New! Experimental (alpha stage) support for Android servers!
New! Children objects lifecycle management in
TMVCActiveRecord(methods
AddChildrenand
RemoveChildren). Really useful to manage child objects such relations or derived properties and are safe in case of multiple addition of the same object as children.
//Having the following declarationtype [MVCNameCase(ncCamelCase)] [MVCTable('authors')] TAuthor = class(TPersonEntityBase) private fBooks: TEnumerable; [MVCTableField('full_name')] fFullName: string; function GetBooks: TEnumerable; public [MVCNameAs('full_name')] property FullName: string read fFullName write fFullName; property Books: TEnumerable read GetBooks; end;
//method GetBooks can be implemented as follows:
implementation
function TAuthor.GetBooks: TEnumerable; begin if fBooks = nil then begin fBooks := TMVCActiveRecord.Where('author_id = ?', [ID]); AddChildren(fBooks); //fBooks will be freed when self will be freed end; Result := fBooks; end;
JSON-RPC Improvements
TMVCJSONRPCExecutor.ConfigHTTPClientto fully customize the inner
THTTPClient(e.g.
ConnectionTimeout,
ResponseTimeoutand so on)
MVCInheritableattribute.
/describereturns the methods list available for that endpoint.
procedure TMainForm.btnSubtractClick(Sender: TObject); var lReq: IJSONRPCRequest; lResp: IJSONRPCResponse; begin lReq := TJSONRPCRequest.Create; lReq.Method := 'subtract'; lReq.RequestID := Random(1000); lReq.Params.Add(StrToInt(edtValue1.Text)); lReq.Params.Add(StrToInt(edtValue2.Text)); lResp := FExecutor.ExecuteRequest(lReq); edtResult.Text := lResp.Result.AsInteger.ToString; end;
procedure TMainForm.btnSubtractWithNamedParamsClick(Sender: TObject); var lReq: IJSONRPCRequest; lResp: IJSONRPCResponse; begin lReq := TJSONRPCRequest.Create; lReq.Method := 'subtract'; lReq.RequestID := Random(1000); lReq.Params.AddByName('Value1', StrToInt(Edit1.Text)); lReq.Params.AddByName('Value2', StrToInt(Edit2.Text)); lResp := FExecutor.ExecuteRequest(lReq); Edit3.Text := lResp.Result.AsInteger.ToString; end;
//Called before as soon as the HTTP arrives procedure TMyPublishedObject.OnBeforeRouting(const JSON: TJDOJsonObject);//Called before the invoked method procedure TMyPublishedObject.OnBeforeCall(const JSONRequest: TJDOJsonObject);
//Called just before to send response to the client procedure TMyPublishedObject.OnBeforeSendResponse(const JSONResponse: TJDOJsonObject);
Deprecated!
TDataSetHolderis deprecated! Use the shining new
ObjectDict(boolean)instead.
New! ObjectDict function is the suggested way to render all the most common data types. It returns a
IMVCObjectDictionarywhich is automatically rendered by the renders. Check the
renders.dprojsample. Here's some example of the shining new
ObjectDict().
Example 1: Rendering a list of objects not freeing them after rendering
Classic
procedure TRenderSampleController.GetLotOfPeople; begin Render(GetPeopleList, False); end;
New approach with ObjectDict
procedure TRenderSampleController.GetLotOfPeople; begin Render(ObjectDict(False).Add('data', GetPeopleList)); end;
Example 2: Rendering a list of objects and automatically free them after rendering
Classic
procedure TRenderSampleController.GetLotOfPeople; begin Render(GetPeopleList); end;
New approach with ObjectDict
procedure TRenderSampleController.GetLotOfPeople; begin Render(ObjectDict().Add('data', GetPeopleList)); end;
Example 3: Rendering a list of objects adding links for HATEOAS support
Classic
procedure TRenderSampleController.GetPeople_AsObjectList_HATEOAS; var p: TPerson; People: TObjectList; begin People := TObjectList.Create(True);{$REGION 'Fake data'} p := TPerson.Create; p.FirstName := 'Daniele'; p.LastName := 'Teti'; p.DOB := EncodeDate(1979, 8, 4); p.Married := True; People.Add(p);
p := TPerson.Create; p.FirstName := 'John'; p.LastName := 'Doe'; p.DOB := EncodeDate(1879, 10, 2); p.Married := False; People.Add(p); p := TPerson.Create; p.FirstName := 'Jane'; p.LastName := 'Doe'; p.DOB := EncodeDate(1883, 1, 5); p.Married := True; People.Add(p);
{$ENDREGION}
Render<tperson>(People, True, procedure(const APerson: TPerson; const Links: IMVCLinks) begin Links .AddRefLink .Add(HATEOAS.HREF, '/people/' + APerson.ID.ToString) .Add(HATEOAS.REL, 'self') .Add(HATEOAS._TYPE, 'application/json') .Add('title', 'Details for ' + APerson.FullName); Links .AddRefLink .Add(HATEOAS.HREF, '/people') .Add(HATEOAS.REL, 'people') .Add(HATEOAS._TYPE, 'application/json'); end);
end;
New approach with ObjectDict
procedure TRenderSampleController.GetPeople_AsObjectList_HATEOAS; var p: TPerson; People: TObjectList; begin People := TObjectList.Create(True);{$REGION 'Fake data'} p := TPerson.Create; p.FirstName := 'Daniele'; p.LastName := 'Teti'; p.DOB := EncodeDate(1979, 8, 4); p.Married := True; People.Add(p);
p := TPerson.Create; p.FirstName := 'John'; p.LastName := 'Doe'; p.DOB := EncodeDate(1879, 10, 2); p.Married := False; People.Add(p); p := TPerson.Create; p.FirstName := 'Jane'; p.LastName := 'Doe'; p.DOB := EncodeDate(1883, 1, 5); p.Married := True; People.Add(p);
{$ENDREGION}
Render(ObjectDict().Add('data', People, procedure(const APerson: TObject; const Links: IMVCLinks) begin Links .AddRefLink .Add(HATEOAS.HREF, '/people/' + TPerson(APerson).ID.ToString) .Add(HATEOAS.REL, 'self') .Add(HATEOAS._TYPE, 'application/json') .Add('title', 'Details for ' + TPerson(APerson).FullName); Links .AddRefLink .Add(HATEOAS.HREF, '/people') .Add(HATEOAS.REL, 'people') .Add(HATEOAS._TYPE, 'application/json'); end));
end;
ObjectDictis able to render multiple data sources (datasets, objectlists, objects or StrDict) at the same time using different casing, HATEOAS callbacks and modes.
procedure TTestServerController.TestObjectDict; var lDict: IMVCObjectDictionary; begin lDict := ObjectDict(false) .Add('ncUpperCase_List', GetDataSet, nil, dstAllRecords, ncUpperCase) .Add('ncLowerCase_List', GetDataSet, nil, dstAllRecords, ncLowerCase) .Add('ncCamelCase_List', GetDataSet, nil, dstAllRecords, ncCamelCase) .Add('ncPascalCase_List', GetDataSet, nil, dstAllRecords, ncPascalCase) .Add('ncUpperCase_Single', GetDataSet, nil, dstSingleRecord, ncUpperCase) .Add('ncLowerCase_Single', GetDataSet, nil, dstSingleRecord, ncLowerCase) .Add('ncCamelCase_Single', GetDataSet, nil, dstSingleRecord, ncCamelCase) .Add('ncPascalCase_Single', GetDataSet, nil, dstSingleRecord, ncPascalCase) .Add('meta', StrDict(['page'], ['1'])); Render(lDict); end;
ObjectDict is the suggested way to renders data. However, the other ones are still there and works as usual.
type TMonthEnum = (meJanuary, meFebruary, meMarch, meApril);TEntityWithEnums = class private FMonthMappedNames: TMonthEnum; FMonthEnumName: TMonthEnum; FMonthOrder: TMonthEnum; public // List items separated by comma or semicolon [MVCEnumSerializationType(estEnumMappedValues, 'January,February,March,April')] property MonthMappedNames: TMonthEnum read FMonthMappedNames write FMonthMappedNames; [MVCEnumSerializationType(estEnumName)] property MonthEnumName: TMonthEnum read FMonthEnumName write FMonthEnumName; [MVCEnumSerializationType(estEnumOrd)] property MonthOrder: TMonthEnum read FMonthOrder write FMonthOrder; end;
...
New Installation procedure!
dmvcframeworkDT)
C:\DEV\dmvcframeworkis the
dmvcframeworkmain folder)
C:\DEV\dmvcframework\sources
C:\DEV\dmvcframework\lib\loggerpro
C:\DEV\dmvcframework\lib\swagdoc\Source
C:\DEV\dmvcframework\lib\dmustache
| Delphi Version | Project Group | | ------------------- | --------------------------------------------- | | Delphi 10.4 Sydney |
packages\d104\dmvcframework_group.groupproj| | Delphi 10.3 Rio |
packages\d103\dmvcframework_group.groupproj| | Delphi 10.2 Tokyo |
packages\d102\dmvcframework_group.groupproj| | Delphi 10.1 Berlin |
packages\d101\dmvcframework_group.groupproj| | Delphi 10.0 Seattle |
packages\d100\dmvcframework_group.groupproj|
In
MVCActiveRecordattribute
MVCPrimaryKeyhas been removed and merged with
MVCTableField, so now
TMVCActiveRecordFieldOptionis a set of
foPrimaryKey,
foAutoGenerated,
foTransient(check
activerecord_showcase.dprojsample).
Middleware
OnAfterControllerActionare now invoked in the same order of
OnBeforeControllerAction(previously were invoked in reversed order).
TMVCEngineis no more responsible for static file serving. If you need static files used the new
TMVCStaticFilesMiddleware(check the sample). As consequence
TMVCConfigKey.DocumentRoot,
TMVCConfigKey.IndexDocumentand
TMVCConfigKey.FallbackResourceare no more available.
TMVCEngine.Configproperty is now read-only. Can be changed only in the anonymous method injected in the constructor.
// This is valid //////////////////////////////////////// FMVC := TMVCEngine.Create(Self, procedure(Config: TMVCConfig) begin // session timeout (0 means session cookie) Config[TMVCConfigKey.SessionTimeout] := '0'; //Other Configurations end); FMVC.AddController(TMyController);// This is not valid (exception is raised) /////////////////////////////////////////
FMVC := TMVCEngine.Create(Self);, FMVC.Config[TMVCConfigKey.SessionTimeout] := '0'; {run-time error here} FMVC.AddController(TMyController);
TMVCActiveRecordframework
TMVCActiveRecordControllerwith automatic RESTful interface generation and permissions handling
TMVCActiveRecordControllerto handle complex cases
TDataSetHolder
404and
500status code returns always a
text/plaincontent-type
MAX_REQUEST_SIZEcan now limit the size of the incoming HTTP requests.
TMVCResponsecan handle generic (non error) responses
gzipcompression support in addition to
deflatein
TCompressionMiddleware
TCompressionMiddlewarehas been renamed in
TMVCCompressionMiddleware
samples\renders_spring4d_nullables)
TMVCJSONRPCPublisherallows to easily expose plain Delphi objects (and even data modules) through a JSON-RPC 2.0 interface!
TJSONBool(Delphi XE8 or older)
DMVCFRAMEWORK_VERSIONdefined in
MVCFramework.Commons.pas
DelphiMVCFramework roadmap is always updated as-soon-as the features planned are implemented. Check the roadmap here.
As you know, good support on open source software is a must for professional users. If you need trainings, consultancy or custom developments on DelphiMVCFramework, send an email to dmvcframework at bittime dot it. Alternatively you can send a request using the contacts forms on bit Time Professionals website. bit Time Professionals is the company behind DelphiMVCFramework, all the main developers works there.
DMVCFramework is provided with a lot of examples focused on specific functionality. All samples are in Samples folder.
DMVCFramework allows to create powerful RESTful servers without effort. You can create a full-flagged RESTful server in a couple of clicks.
DMVCFramework must be installed using the Github release. Download the zip file
Unzip it the release zip in a folder named
C:\dmvc(or where you prefer).
Launch RAD Studio and open
C:\dmvc\packages\d104\dmvcframework_group.groupproj
WARNING! In the last path shown "d104" is for Delphi 10.4 Sydney. Use the correct package for your Delphi version.
Install the package and close all
Now, DMVCFramework expert is installed and you are able to create DMVCFramework project. Go to go to
File->New->Otherselect
Delphi Project->DMVC->DelphiMVCFramework Project
OK
Tools->Options->Language->Delphi->Libraryand add in the
Library Paththe following paths:
Below a basic sample of a DMVCFramework controller with 2 action
unit UsersControllerU;interface
uses MVCFramework;
type [MVCPath('/users')] TUsersController = class(TMVCController) public
//The following action will be with a GET request like the following //http://myserver.com/users/3 [MVCPath('/($id)')] [MVCProduces('application/json')] [MVCHTTPMethod([httpGET])] [MVCDoc('Returns a user as a JSON object')] procedure GetUser(id: Integer); //The following action will be with a GET request like the following //http://myserver.com/users [MVCPath('/')] [MVCProduces('application/json')] [MVCHTTPMethod([httpGET])] [MVCDoc('Returns the users list as a JSON Array of JSON Objects')] procedure GetUsers; //The following action will be with a PUT request like the following //http://myserver.com/users/3 //and in the request body there should be a serialized TUser [MVCPath('/($id)')] [MVCProduce('application/json')] [MVCHTTPMethod([httpPUT])] [MVCDoc('Update a user')] procedure UpdateUser(id: Integer); //The following action will respond to a POST request like the following //http://myserver.com/users //and in the request body there should be the new user to create as json [MVCPath] [MVCProduce('application/json')] [MVCHTTPMethod([httpPOST])] [MVCDoc('Create a new user, returns the id of the new user')] procedure CreateUser;
end;
implementation
uses MyTransactionScript; //contains actual data access code
{ TUsersController }
procedure TUsersController.GetUsers; var Users: TObjectList; begin Users := GetUsers; Render(Users); end;
procedure TUsersController.GetUser(id: Integer); var User: TUser; begin User := GetUserById(id); Render(User); end;
procedure TUsersController.UpdateUser(id: Integer); var User: TUser; begin User := Context.Request.BodyAs; UpdateUser(id, User); Render(User); end;
procedure TUsersController.CreateUser; var User: TUser; begin User := Context.Request.BodyAs; CreateUser(User); Render(User); end;
end.
Now you have a performant RESTful server wich respond to the following URLs: - GET /users/($id) (eg. /users/1, /users/45 etc) - PUT /users/($id) (eg. /users/1, /users/45 etc with the JSON data in the request body) - POST /users (the JSON data must be in the request body)
If you don't plan to deploy your DMVCFramework server behind a webserver (apache or IIS) you can also pack more than one listener application server into one single executable. In this case, the process is a bit different and involves the creation of a listener context. However, create a new server is a simple task:
uses MVCFramework.Server, MVCFramework.Server.Impl;var LServerListener: IMVCListener; begin LServerListener := TMVCListener.Create(TMVCListenerProperties.New .SetName('Listener1') .SetPort(5000) .SetMaxConnections(1024) .SetWebModuleClass(YourServerWebModuleClass) );
LServerListener.Start; LServerListener.Stop; end;
If you want to add a layer of security (in its WebModule you should add the security middleware):
uses MVCFramework.Server, MVCFramework.Server.Impl, MVCFramework.Middleware.Authentication;procedure TTestWebModule.WebModuleCreate(Sender: TObject); begin FMVCEngine := TMVCEngine.Create(Self);
// Add Yours Controllers FMVCEngine.AddController(TYourController);
// Add Security Middleware FMVCEngine.AddMiddleware(TMVCBasicAuthenticationMiddleware.Create( TMVCDefaultAuthenticationHandler.New .SetOnAuthentication( procedure(const AUserName, APassword: string; AUserRoles: TList; var IsValid: Boolean; const ASessionData: TDictionary) begin IsValid := AUserName.Equals('dmvc') and APassword.Equals('123'); end ) )); end;
In stand alone mode you can work with a context that supports multiple listeners servers:
uses MVCFramework.Server, MVCFramework.Server.Impl;var LServerListenerCtx: IMVCListenersContext;
begin LServerListenerCtx := TMVCListenersContext.Create;
LServerListenerCtx.Add(TMVCListenerProperties.New .SetName('Listener1') .SetPort(6000) .SetMaxConnections(1024) .SetWebModuleClass(WebModuleClass1) );
LServerListenerCtx.Add(TMVCListenerProperties.New .SetName('Listener2') .SetPort(7000) .SetMaxConnections(1024) .SetWebModuleClass(WebModuleClass2) );
LServerListenerCtx.StartAll; end;
Resource Query Language (RQL) is a query language designed for use in URIs with object style data structures. DMVCFramework supports RQL natively and the included MVCActiveRecord micro-framework, implement a large subset of the RQL specs.
RQL can be thought as basically a set of nestable named operators which each have a set of arguments. RQL is designed to have an extremely simple, but extensible grammar that can be written in a URL friendly query string. A simple RQL query with a single operator that indicates a search for any resources with a property of "foo" that has value of 5 could be written:
eq(foo,5)
A more complex filter can include an arbitrary number of chained functions
or(and(eq(name,"daniele"),eq(surname,"teti")),and(eq(name,"peter"),eq(surname,"parker"));sort(+name)
Which is translated (details depends from the RDBMS) in the following SQL.
select name, surname {other fields} from people where (name = "daniele" and surname = "teti") or (name="peter" and surname = "parker") order by name asc
RQL is designed for modern application development. It is built for the web, ready for NoSQL, and highly extensible with simple syntax.
Here is a definition of the common operators as implemented in DMVCFramework' ActiveRecord:
eq(,) - Filters for objects where the specified property's value is equal to the provided value lt(,) - Filters for objects where the specified property's value is less than the provided value le(,) - Filters for objects where the specified property's value is less than or equal to the provided value gt(,) - Filters for objects where the specified property's value is greater than the provided value ge(,) - Filters for objects where the specified property's value is greater than or equal to the provided value ne(,) - Filters for objects where the specified property's value is not equal to the provided value and(,,...) - Applies all the given queries or(,,...) - The union of the given queries sort(,) - Filters for objects where the specified property's value is an array and the array contains any value that equals the provided value or satisfies the provided expression. in(,) - Filters for objects where the specified property's value is in the provided array out(,) - Filters for objects where the specified property's value is not in the provided array
Not Yet Availables
select(,,...) - Trims each object down to the set of properties defined in the arguments values() - Returns an array of the given property value for each object aggregate(,...) - Aggregates the array, grouping by objects that are distinct for the provided properties, and then reduces the remaining other property values using the provided functions distinct() - Returns a result set with duplicates removed excludes(,) - Filters for objects where the specified property's value is an array and the array does not contain any of value that equals the provided value or satisfies the provided expression. rel(,) - Applies the provided query against the linked data of the provided relation name. sum() - Finds the sum of every value in the array or if the property argument is provided, returns the sum of the value of property for every object in the array mean() - Finds the mean of every value in the array or if the property argument is provided, returns the mean of the value of property for every object in the array max() - Finds the maximum of every value in the array or if the property argument is provided, returns the maximum of the value of property for every object in the array min() - Finds the minimum of every value in the array or if the property argument is provided, returns the minimum of the value of property for every object in the array recurse() - Recursively searches, looking in children of the object as objects in arrays in the given property value first() - Returns the first record of the query's result set one() - Returns the first and only record of the query's result set, or produces an error if the query's result set has more or less than one record in it. count() - Returns the count of the number of records in the query's result set
Feel free to ask questions on the "Delphi MVC Framework" facebook group (https://www.facebook.com/groups/delphimvcframework).