mirror of
https://github.com/danieleteti/delphimvcframework.git
synced 2024-11-15 07:45:54 +01:00
Merge branch 'feature_injector'
This commit is contained in:
commit
5dfb0b9a50
@ -120,13 +120,16 @@ resourcestring
|
||||
' end);' + sLineBreak +
|
||||
'' + sLineBreak +
|
||||
' WebRequestHandlerProc.MaxConnections := dotEnv.Env(''dmvc.handler.max_connections'', 1024);' + sLineBreak +
|
||||
'' + sLineBreak +
|
||||
' if dotEnv.Env(''dmvc.profiler.enabled'', false) then' + sLineBreak +
|
||||
' begin' + sLineBreak +
|
||||
' Profiler.ProfileLogger := Log;' + sLineBreak +
|
||||
' Profiler.WarningThreshold := dotEnv.Env(''dmvc.profiler.warning_threshold'', 2000);' + sLineBreak +
|
||||
' end;' + sLineBreak +
|
||||
'' + sLineBreak +
|
||||
'' + sLineBreak +
|
||||
'' + sLineBreak +
|
||||
'{$IF CompilerVersion >= 34}' + sLineBreak +
|
||||
' if dotEnv.Env(''dmvc.profiler.enabled'', false) then' + sLineBreak +
|
||||
' begin' + sLineBreak +
|
||||
' Profiler.ProfileLogger := Log;' + sLineBreak +
|
||||
' Profiler.WarningThreshold := dotEnv.Env(''dmvc.profiler.warning_threshold'', 2000);' + sLineBreak +
|
||||
' end;' + sLineBreak +
|
||||
'{$ENDIF}' + sLineBreak +
|
||||
'' + sLineBreak +
|
||||
' RunServer(dotEnv.Env(''dmvc.server.port'', %1:d));' + sLineBreak +
|
||||
' except' + sLineBreak +
|
||||
' on E: Exception do' + sLineBreak +
|
||||
|
@ -116,7 +116,8 @@ contains
|
||||
MVCFramework.DotEnv in '..\..\sources\MVCFramework.DotEnv.pas',
|
||||
MVCFramework.Serializer.URLEncoded in '..\..\sources\MVCFramework.Serializer.URLEncoded.pas',
|
||||
MVCFramework.Signal in '..\..\sources\MVCFramework.Signal.pas',
|
||||
MVCFramework.Serializer.Text in '..\..\sources\MVCFramework.Serializer.Text.pas';
|
||||
MVCFramework.Serializer.Text in '..\..\sources\MVCFramework.Serializer.Text.pas',
|
||||
MVCFramework.Container in '..\..\sources\MVCFramework.Container.pas';
|
||||
|
||||
end.
|
||||
|
||||
|
@ -208,6 +208,7 @@
|
||||
<DCCReference Include="..\..\sources\MVCFramework.Serializer.URLEncoded.pas"/>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.Signal.pas"/>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.Serializer.Text.pas"/>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.Container.pas"/>
|
||||
<BuildConfiguration Include="Base">
|
||||
<Key>Base</Key>
|
||||
</BuildConfiguration>
|
||||
|
@ -319,6 +319,15 @@ object MainForm: TMainForm
|
||||
TabOrder = 31
|
||||
OnClick = btnCRUDWithOptionsClick
|
||||
end
|
||||
object btnTransaction: TButton
|
||||
Left = 144
|
||||
Top = 602
|
||||
Width = 121
|
||||
Height = 35
|
||||
Caption = 'TransactionContext'
|
||||
TabOrder = 32
|
||||
OnClick = btnTransactionClick
|
||||
end
|
||||
object FDConnection1: TFDConnection
|
||||
Left = 312
|
||||
Top = 40
|
||||
|
@ -27,6 +27,8 @@ uses
|
||||
FireDAC.Comp.Client,
|
||||
MVCFramework.Nullables,
|
||||
MVCFramework.ActiveRecord,
|
||||
MVCFramework.Logger,
|
||||
|
||||
System.Generics.Collections, System.Diagnostics;
|
||||
|
||||
type
|
||||
@ -64,6 +66,7 @@ type
|
||||
btnObjectVersion: TButton;
|
||||
btnCustomTable: TButton;
|
||||
btnCRUDWithOptions: TButton;
|
||||
btnTransaction: TButton;
|
||||
procedure btnCRUDClick(Sender: TObject);
|
||||
procedure btnInheritanceClick(Sender: TObject);
|
||||
procedure btnMultiThreadingClick(Sender: TObject);
|
||||
@ -98,9 +101,11 @@ type
|
||||
procedure btnObjectVersionClick(Sender: TObject);
|
||||
procedure btnCustomTableClick(Sender: TObject);
|
||||
procedure btnCRUDWithOptionsClick(Sender: TObject);
|
||||
procedure btnTransactionClick(Sender: TObject);
|
||||
private
|
||||
procedure Log(const Value: string);
|
||||
procedure LoadCustomers(const HowManyCustomers: Integer = 50);
|
||||
procedure ExecutedInTransaction;
|
||||
public
|
||||
{ Public declarations }
|
||||
end;
|
||||
@ -1896,6 +1901,54 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnTransactionClick(Sender: TObject);
|
||||
begin
|
||||
Log('# TransactionContext');
|
||||
|
||||
|
||||
// Test 1
|
||||
// try
|
||||
// begin var Ctx := TMVCActiveRecord.UseTransactionContext;
|
||||
// TMVCActiveRecord.GetByPK<TCustomer>(-1); // will raise EMVCActiveRecordNotFound
|
||||
// end;
|
||||
// except
|
||||
// on E: Exception do
|
||||
// begin
|
||||
// Log(Format('#1 - TransactionContext caught %s (automatic rollback)', [E.ClassName]));
|
||||
// end;
|
||||
// end;
|
||||
|
||||
|
||||
// Test 2
|
||||
// try
|
||||
// begin var Ctx := TMVCActiveRecord.UseTransactionContext;
|
||||
// var S := Ctx; // will raise EMVCActiveRecordTransactionContext
|
||||
// end;
|
||||
// except
|
||||
// on E: Exception do
|
||||
// begin
|
||||
// Log(Format('#2 - TransactionContext caught %s (automatic rollback)', [E.ClassName]));
|
||||
// end;
|
||||
// end;
|
||||
|
||||
|
||||
// Test 3
|
||||
// begin var Ctx := TMVCActiveRecord.UseTransactionContext;
|
||||
// var lCustomer := TCustomer.Create;
|
||||
// try
|
||||
// lCustomer.CompanyName := 'Transaction Inc.';
|
||||
// lCustomer.LastContact := Now();
|
||||
// lCustomer.Insert;
|
||||
// finally
|
||||
// lCustomer.Free;
|
||||
// end;
|
||||
// Log('#3 - TransactionContext automatically committed changes (because no exceptions have been raised within the TransactionContext)');
|
||||
// end;
|
||||
|
||||
// Test 4
|
||||
ExecutedInTransaction;
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnReadOnlyFieldsClick(Sender: TObject);
|
||||
var
|
||||
lCustomer: TCustomerWithReadOnlyFields;
|
||||
@ -2126,6 +2179,20 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ExecutedInTransaction;
|
||||
begin
|
||||
var tx := TMVCActiveRecord.UseTransactionContext;
|
||||
var lCustomer := TCustomer.Create;
|
||||
try
|
||||
lCustomer.CompanyName := 'Transaction Inc.';
|
||||
lCustomer.LastContact := Now();
|
||||
lCustomer.Insert;
|
||||
finally
|
||||
lCustomer.Free;
|
||||
end;
|
||||
Log('#4 - TransactionContext automatically committed changes (because no exceptions have been raised within the TransactionContext)');
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnObjectVersionClick(Sender: TObject);
|
||||
begin
|
||||
var lID: NullableInt64;
|
||||
|
@ -3,46 +3,51 @@ unit BusinessObjects;
|
||||
interface
|
||||
|
||||
uses
|
||||
MVCFramework.Serializer.Commons, MVCFramework.Nullables;
|
||||
MVCFramework.Serializer.Commons, MVCFramework.Nullables,
|
||||
MVCFramework.ActiveRecord;
|
||||
|
||||
type
|
||||
TBaseBO = class
|
||||
TBaseBO = class(TMVCActiveRecord)
|
||||
private
|
||||
[MVCTableField('ID', [foPrimaryKey])]
|
||||
FID: Integer;
|
||||
procedure SetID(const Value: Integer);
|
||||
public
|
||||
procedure CheckInsert; virtual;
|
||||
procedure CheckUpdate; virtual;
|
||||
procedure CheckDelete; virtual;
|
||||
property ID: Integer read FID write SetID;
|
||||
end;
|
||||
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
[MVCTable('ARTICOLI')]
|
||||
[MVCNamedSQLQuery(
|
||||
'search_by_text',
|
||||
'SELECT * FROM ARTICOLI WHERE DESCRIZIONE CONTAINING ? ORDER BY ID')
|
||||
]
|
||||
TArticle = class(TBaseBO)
|
||||
private
|
||||
FPrice: Currency;
|
||||
[MVCTableField('CODICE')]
|
||||
FCode: string;
|
||||
[MVCTableField('DESCRIZIONE')]
|
||||
FDescription: String;
|
||||
[MVCTableField('PREZZO')]
|
||||
FPrice: Currency;
|
||||
[MVCTableField('UPDATED_AT')]
|
||||
FUpdatedAt: TDateTime;
|
||||
[MVCTableField('CREATED_AT')]
|
||||
FCreatedAt: TDateTime;
|
||||
procedure SetCode(const Value: string);
|
||||
procedure SetDescription(const Value: String);
|
||||
procedure SetPrice(const Value: Currency);
|
||||
procedure SetCreatedAt(const Value: TDateTime);
|
||||
procedure SetUpdatedAt(const Value: TDateTime);
|
||||
protected
|
||||
procedure OnBeforeInsertOrUpdate; override;
|
||||
procedure OnBeforeUpdate; override;
|
||||
procedure OnBeforeDelete; override;
|
||||
public
|
||||
procedure CheckInsert; override;
|
||||
procedure CheckUpdate; override;
|
||||
procedure CheckDelete; override;
|
||||
[MVCColumn('CODICE')]
|
||||
property Code: string read FCode write SetCode;
|
||||
[MVCColumn('DESCRIZIONE')]
|
||||
property Description: String read FDescription write SetDescription;
|
||||
[MVCColumn('PREZZO')]
|
||||
property Price: Currency read FPrice write SetPrice;
|
||||
[MVCColumn('CREATED_AT')]
|
||||
property CreatedAt: TDateTime read FCreatedAt write SetCreatedAt;
|
||||
[MVCColumn('UPDATED_AT')]
|
||||
property UpdatedAt: TDateTime read FUpdatedAt write SetUpdatedAt;
|
||||
end;
|
||||
|
||||
@ -53,46 +58,28 @@ uses
|
||||
|
||||
{ TBaseBO }
|
||||
|
||||
procedure TBaseBO.CheckDelete;
|
||||
begin
|
||||
|
||||
end;
|
||||
|
||||
procedure TBaseBO.CheckInsert;
|
||||
begin
|
||||
|
||||
end;
|
||||
|
||||
procedure TBaseBO.CheckUpdate;
|
||||
begin
|
||||
|
||||
end;
|
||||
|
||||
procedure TBaseBO.SetID(const Value: Integer);
|
||||
begin
|
||||
FID := Value;
|
||||
end;
|
||||
|
||||
{ TArticolo }
|
||||
|
||||
procedure TArticle.CheckDelete;
|
||||
procedure TArticle.OnBeforeDelete;
|
||||
begin
|
||||
inherited;
|
||||
if Price <= 5 then
|
||||
raise Exception.Create('Cannot delete an article with a price below 5 euros (yes, it is a silly check)');
|
||||
end;
|
||||
|
||||
procedure TArticle.CheckInsert;
|
||||
procedure TArticle.OnBeforeInsertOrUpdate;
|
||||
begin
|
||||
inherited;
|
||||
if not TRegEx.IsMatch(Code, '^C[0-9]{2,4}') then
|
||||
raise Exception.Create('Article code must be in the format "CXX or CXXX or CXXXX"');
|
||||
end;
|
||||
|
||||
procedure TArticle.CheckUpdate;
|
||||
procedure TArticle.OnBeforeUpdate;
|
||||
begin
|
||||
inherited;
|
||||
CheckInsert;
|
||||
if Price <= 2 then
|
||||
raise Exception.Create('We cannot sell so low cost pizzas!');
|
||||
end;
|
||||
|
@ -10,6 +10,9 @@ type
|
||||
|
||||
end;
|
||||
|
||||
const
|
||||
CON_DEF_NAME = 'MyConnX';
|
||||
|
||||
implementation
|
||||
|
||||
end.
|
||||
|
@ -3,37 +3,42 @@ unit Controllers.Articles;
|
||||
interface
|
||||
|
||||
uses
|
||||
mvcframework,
|
||||
mvcframework.Commons,
|
||||
mvcframework.Serializer.Commons,
|
||||
MVCFramework,
|
||||
MVCFramework.Commons,
|
||||
System.Generics.Collections,
|
||||
Controllers.Base, BusinessObjects;
|
||||
Controllers.Base,
|
||||
BusinessObjects,
|
||||
Services;
|
||||
|
||||
type
|
||||
|
||||
[MVCDoc('Resource that manages articles CRUD')]
|
||||
[MVCPath('/articles')]
|
||||
TArticlesController = class(TBaseController)
|
||||
private
|
||||
fArticlesService: IArticlesService;
|
||||
public
|
||||
[MVCInject]
|
||||
constructor Create(ArticlesService: IArticlesService); reintroduce;
|
||||
|
||||
[MVCDoc('Returns the list of articles')]
|
||||
[MVCPath]
|
||||
[MVCHTTPMethod([httpGET])]
|
||||
procedure GetArticles;
|
||||
function GetArticles: IMVCResponse;
|
||||
|
||||
[MVCDoc('Returns the list of articles')]
|
||||
[MVCPath('/searches')]
|
||||
[MVCHTTPMethod([httpGET])]
|
||||
procedure GetArticlesByDescription(const [MVCFromQueryString('q', '')] Search: String);
|
||||
function GetArticlesByDescription(const [MVCFromQueryString('q', '')] Search: String): IMVCResponse;
|
||||
|
||||
[MVCDoc('Returns the article with the specified id')]
|
||||
[MVCPath('/meta')]
|
||||
[MVCHTTPMethod([httpGET])]
|
||||
procedure GetArticleMeta;
|
||||
function GetArticleMeta: IMVCResponse;
|
||||
|
||||
[MVCDoc('Returns the article with the specified id')]
|
||||
[MVCPath('/($id)')]
|
||||
[MVCHTTPMethod([httpGET])]
|
||||
procedure GetArticleByID(id: Integer);
|
||||
function GetArticleByID(id: Integer): IMVCResponse;
|
||||
|
||||
[MVCDoc('Deletes the article with the specified id')]
|
||||
[MVCPath('/($id)')]
|
||||
@ -43,17 +48,17 @@ type
|
||||
[MVCDoc('Updates the article with the specified id and return "200: OK"')]
|
||||
[MVCPath('/($id)')]
|
||||
[MVCHTTPMethod([httpPUT])]
|
||||
procedure UpdateArticleByID(const [MVCFromBody] Article: TArticle; const id: Integer);
|
||||
function UpdateArticleByID(const [MVCFromBody] Article: TArticle; const id: Integer): IMVCResponse;
|
||||
|
||||
[MVCDoc('Creates a new article and returns "201: Created"')]
|
||||
[MVCPath]
|
||||
[MVCHTTPMethod([httpPOST])]
|
||||
procedure CreateArticle(const [MVCFromBody] Article: TArticle);
|
||||
function CreateArticle(const [MVCFromBody] Article: TArticle): IMVCResponse;
|
||||
|
||||
[MVCDoc('Creates new articles from a list and returns "201: Created"')]
|
||||
[MVCPath('/bulk')]
|
||||
[MVCHTTPMethod([httpPOST])]
|
||||
procedure CreateArticles(const [MVCFromBody] ArticleList: TObjectList<TArticle>);
|
||||
function CreateArticles(const [MVCFromBody] ArticleList: TObjectList<TArticle>): IMVCResponse;
|
||||
end;
|
||||
|
||||
implementation
|
||||
@ -61,116 +66,70 @@ implementation
|
||||
{ TArticlesController }
|
||||
|
||||
uses
|
||||
Services,
|
||||
Commons,
|
||||
mvcframework.Serializer.Intf,
|
||||
System.SysUtils;
|
||||
|
||||
procedure TArticlesController.CreateArticle(const Article: TArticle);
|
||||
constructor TArticlesController.Create(ArticlesService: IArticlesService);
|
||||
begin
|
||||
GetArticlesService.Add(Article);
|
||||
inherited Create;
|
||||
fArticlesService := ArticlesService;
|
||||
end;
|
||||
|
||||
function TArticlesController.CreateArticle(const Article: TArticle): IMVCResponse;
|
||||
begin
|
||||
fArticlesService.Add(Article);
|
||||
Render201Created('/articles/' + Article.id.ToString, 'Article Created');
|
||||
end;
|
||||
|
||||
procedure TArticlesController.CreateArticles(const ArticleList: TObjectList<TArticle>);
|
||||
var
|
||||
lArticle: TArticle;
|
||||
function TArticlesController.CreateArticles(const ArticleList: TObjectList<TArticle>): IMVCResponse;
|
||||
begin
|
||||
GetArticlesService.StartTransaction;
|
||||
try
|
||||
for lArticle in ArticleList do
|
||||
begin
|
||||
GetArticlesService.Add(lArticle);
|
||||
end;
|
||||
GetArticlesService.Commit;
|
||||
except
|
||||
GetArticlesService.Rollback;
|
||||
raise;
|
||||
end;
|
||||
Render(201, 'Articles Created');
|
||||
fArticlesService.CreateArticles(ArticleList);
|
||||
Result := MVCResponseBuilder.StatusCode(HTTP_STATUS.Created).Build;
|
||||
end;
|
||||
|
||||
procedure TArticlesController.DeleteArticleByID(id: Integer);
|
||||
var
|
||||
Article: TArticle;
|
||||
begin
|
||||
GetArticlesService.StartTransaction;
|
||||
try
|
||||
Article := GetArticlesService.GetByID(id);
|
||||
try
|
||||
GetArticlesService.Delete(Article);
|
||||
finally
|
||||
Article.Free;
|
||||
end;
|
||||
GetArticlesService.Commit;
|
||||
except
|
||||
GetArticlesService.Rollback;
|
||||
raise;
|
||||
end;
|
||||
fArticlesService.Delete(fArticlesService.GetByID(id));
|
||||
end;
|
||||
|
||||
procedure TArticlesController.GetArticles;
|
||||
function TArticlesController.GetArticles: IMVCResponse;
|
||||
begin
|
||||
Render(ObjectDict().Add('data', GetArticlesService.GetAll));
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.OK)
|
||||
.Body(fArticlesService.GetAll)
|
||||
.Build;
|
||||
end;
|
||||
|
||||
procedure TArticlesController.GetArticlesByDescription(const Search: String);
|
||||
var
|
||||
lDict: IMVCObjectDictionary;
|
||||
function TArticlesController.GetArticlesByDescription(const Search: String): IMVCResponse;
|
||||
begin
|
||||
try
|
||||
if Search = '' then
|
||||
begin
|
||||
lDict := ObjectDict().Add('data', GetArticlesService.GetAll);
|
||||
end
|
||||
else
|
||||
begin
|
||||
lDict := ObjectDict().Add('data', GetArticlesService.GetArticles(Search));
|
||||
end;
|
||||
Render(lDict);
|
||||
except
|
||||
on E: EServiceException do
|
||||
begin
|
||||
raise EMVCException.Create(E.Message, '', 0, 404);
|
||||
end
|
||||
else
|
||||
raise;
|
||||
end;
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.OK)
|
||||
.Body(fArticlesService.GetArticles(Search))
|
||||
.Build;
|
||||
end;
|
||||
|
||||
procedure TArticlesController.UpdateArticleByID(const Article: TArticle; const id: Integer);
|
||||
function TArticlesController.UpdateArticleByID(const Article: TArticle; const id: Integer): IMVCResponse;
|
||||
begin
|
||||
Article.id := id;
|
||||
GetArticlesService.Update(Article);
|
||||
Render(200, 'Article Updated');
|
||||
fArticlesService.Update(Article);
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.OK)
|
||||
.Build;
|
||||
end;
|
||||
|
||||
procedure TArticlesController.GetArticleByID(id: Integer);
|
||||
function TArticlesController.GetArticleByID(id: Integer): IMVCResponse;
|
||||
begin
|
||||
try
|
||||
Render(ObjectDict().Add('data', GetArticlesService.GetByID(id)));
|
||||
except
|
||||
on E: EServiceException do
|
||||
begin
|
||||
raise EMVCException.Create(E.Message, '', 0, 404);
|
||||
end
|
||||
else
|
||||
raise;
|
||||
end;
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.OK)
|
||||
.Body(fArticlesService.GetByID(id))
|
||||
.Build;
|
||||
end;
|
||||
|
||||
procedure TArticlesController.GetArticleMeta;
|
||||
function TArticlesController.GetArticleMeta: IMVCResponse;
|
||||
begin
|
||||
try
|
||||
Render(ObjectDict().Add('data', GetArticlesService.GetMeta));
|
||||
except
|
||||
on E: EServiceException do
|
||||
begin
|
||||
raise EMVCException.Create(E.Message, '', 0, 404);
|
||||
end
|
||||
else
|
||||
raise;
|
||||
end;
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.OK)
|
||||
.Body(fArticlesService.GetMeta)
|
||||
.Build;
|
||||
end;
|
||||
|
||||
end.
|
||||
|
@ -3,19 +3,10 @@ unit Controllers.Base;
|
||||
interface
|
||||
|
||||
uses
|
||||
MVCFramework, MVCFramework.Commons, Services, MainDM;
|
||||
MVCFramework, MVCFramework.Commons, Services;
|
||||
|
||||
type
|
||||
TBaseController = class abstract(TMVCController)
|
||||
strict private
|
||||
FDM: TdmMain;
|
||||
FArticlesService: TArticlesService;
|
||||
function GetDataModule: TdmMain;
|
||||
strict protected
|
||||
function GetArticlesService: TArticlesService;
|
||||
public
|
||||
destructor Destroy; override;
|
||||
|
||||
end;
|
||||
|
||||
[MVCPath('/private')]
|
||||
@ -23,7 +14,7 @@ type
|
||||
public
|
||||
[MVCPath('/articles')]
|
||||
[MVCHTTPMethods([httpDELETE])]
|
||||
procedure DeleteAllArticles;
|
||||
procedure DeleteAllArticles(ArticlesService: IArticlesService);
|
||||
end;
|
||||
|
||||
implementation
|
||||
@ -31,34 +22,11 @@ implementation
|
||||
uses
|
||||
System.SysUtils;
|
||||
|
||||
{ TBaseController }
|
||||
|
||||
destructor TBaseController.Destroy;
|
||||
begin
|
||||
FArticlesService.Free;
|
||||
FDM.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
function TBaseController.GetArticlesService: TArticlesService;
|
||||
begin
|
||||
if not Assigned(FArticlesService) then
|
||||
FArticlesService := TArticlesService.Create(GetDataModule);
|
||||
Result := FArticlesService;
|
||||
end;
|
||||
|
||||
function TBaseController.GetDataModule: TdmMain;
|
||||
begin
|
||||
if not Assigned(FDM) then
|
||||
FDM := TdmMain.Create(nil);
|
||||
Result := FDM;
|
||||
end;
|
||||
|
||||
{ TPrivateController }
|
||||
|
||||
procedure TPrivateController.DeleteAllArticles;
|
||||
procedure TPrivateController.DeleteAllArticles(ArticlesService: IArticlesService);
|
||||
begin
|
||||
GetArticlesService.DeleteAllArticles();
|
||||
ArticlesService.DeleteAllArticles();
|
||||
end;
|
||||
|
||||
end.
|
||||
|
225
samples/articles_crud_server/FDConnectionConfigU.pas
Normal file
225
samples/articles_crud_server/FDConnectionConfigU.pas
Normal file
@ -0,0 +1,225 @@
|
||||
unit FDConnectionConfigU;
|
||||
|
||||
interface
|
||||
|
||||
|
||||
|
||||
procedure CreateFirebirdPrivateConnDef(AIsPooled: boolean);
|
||||
procedure CreateInterbasePrivateConnDef(AIsPooled: boolean);
|
||||
procedure CreateMySQLPrivateConnDef(AIsPooled: boolean);
|
||||
procedure CreateMSSQLServerPrivateConnDef(AIsPooled: boolean);
|
||||
procedure CreatePostgresqlPrivateConnDef(AIsPooled: boolean);
|
||||
procedure CreateSqlitePrivateConnDef(AIsPooled: boolean);
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
MVCFramework.Commons,
|
||||
System.Classes,
|
||||
System.IOUtils,
|
||||
FireDAC.Comp.Client,
|
||||
FireDAC.Moni.Base,
|
||||
FireDAC.Moni.FlatFile,
|
||||
FireDAC.Stan.Intf
|
||||
, Commons;
|
||||
|
||||
|
||||
var
|
||||
gFlatFileMonitor: TFDMoniFlatFileClientLink = nil;
|
||||
|
||||
procedure CreateMySQLPrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
begin
|
||||
{
|
||||
docker run --detach --env MARIADB_USER=example-user --env MARIADB_PASSWORD=my_cool_secret --env MARIADB_ROOT_PASSWORD=root -p 3306:3306 mariadb:latest
|
||||
}
|
||||
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
LParams.Add('Database=activerecorddb');
|
||||
LParams.Add('Protocol=TCPIP');
|
||||
LParams.Add('Server=localhost');
|
||||
LParams.Add('User_Name=root');
|
||||
LParams.Add('Password=root');
|
||||
LParams.Add('TinyIntFormat=Boolean'); { it's the default }
|
||||
LParams.Add('CharacterSet=utf8mb4'); // not utf8!!
|
||||
LParams.Add('MonitorBy=FlatFile');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'MySQL', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CreateMSSQLServerPrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
begin
|
||||
{
|
||||
docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=!SA_password!" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-latest
|
||||
}
|
||||
|
||||
// [ACTIVERECORDB_SQLSERVER]
|
||||
// Database=activerecorddb
|
||||
// OSAuthent=Yes
|
||||
// Server=DANIELETETI\SQLEXPRESS
|
||||
// DriverID=MSSQL
|
||||
//
|
||||
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
LParams.Add('Database=activerecorddb');
|
||||
LParams.Add('OSAuthent=Yes');
|
||||
LParams.Add('Server=DANIELETETI\SQLEXPRESS');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'MSSQL', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CreateFirebirdPrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
begin
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
LParams.Add('Database=' + dotEnv.Env('database.path'));
|
||||
LParams.Add('Protocol=TCPIP');
|
||||
LParams.Add('Server=localhost');
|
||||
LParams.Add('User_Name=sysdba');
|
||||
LParams.Add('Password=masterkey');
|
||||
LParams.Add('CharacterSet=UTF8');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'FB', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CreateInterbasePrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
begin
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
LParams.Add('Database=' + TPath.GetFullPath(TPath.Combine('..\..',
|
||||
'data\ACTIVERECORDDB.IB')));
|
||||
LParams.Add('Protocol=TCPIP');
|
||||
LParams.Add('Server=localhost');
|
||||
LParams.Add('User_Name=sysdba');
|
||||
LParams.Add('Password=masterkey');
|
||||
LParams.Add('CharacterSet=UTF8');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'IB', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CreatePostgresqlPrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
begin
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
LParams.Add('Database=activerecorddb');
|
||||
LParams.Add('Protocol=TCPIP');
|
||||
LParams.Add('Server=localhost');
|
||||
LParams.Add('User_Name=postgres');
|
||||
LParams.Add('Password=postgres');
|
||||
// LParams.Add('MonitorBy=FlatFile');
|
||||
|
||||
// https://quality.embarcadero.com/browse/RSP-19755?jql=text%20~%20%22firedac%20guid%22
|
||||
LParams.Add('GUIDEndian=Big');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'PG', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CreateSqlitePrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
lFName: string;
|
||||
begin
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
lFName := TPath.Combine(TPath.GetDirectoryName(ParamStr(0)),
|
||||
'..\..\data\activerecorddb.db');
|
||||
LParams.Add('Database=' + lFName);
|
||||
LParams.Add('StringFormat=Unicode');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'SQLite', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
initialization
|
||||
|
||||
gFlatFileMonitor := TFDMoniFlatFileClientLink.Create(nil);
|
||||
gFlatFileMonitor.FileColumns := [tiRefNo, tiTime, tiThreadID, tiClassName, tiObjID, tiMsgText];
|
||||
gFlatFileMonitor.EventKinds := [
|
||||
ekVendor, ekConnConnect, ekLiveCycle, ekError, ekConnTransact,
|
||||
ekCmdPrepare, ekCmdExecute, ekCmdDataIn, ekCmdDataOut];
|
||||
gFlatFileMonitor.ShowTraces := False;
|
||||
gFlatFileMonitor.FileAppend := False;
|
||||
gFlatFileMonitor.FileName := TPath.ChangeExtension(ParamStr(0), '.trace.log');
|
||||
gFlatFileMonitor.Tracing := True;
|
||||
|
||||
finalization
|
||||
|
||||
gFlatFileMonitor.Free;
|
||||
|
||||
end.
|
@ -23,10 +23,10 @@ uses
|
||||
FireDAC.DApt,
|
||||
FireDAC.Comp.DataSet,
|
||||
FireDAC.Phys.FBDef,
|
||||
FireDAC.VCLUI.Wait;
|
||||
FireDAC.VCLUI.Wait, Services;
|
||||
|
||||
type
|
||||
TdmMain = class(TDataModule)
|
||||
TdmMain = class(TDataModule, IArticlesDataModule)
|
||||
Connection: TFDConnection;
|
||||
dsArticles: TFDQuery;
|
||||
updArticles: TFDUpdateSQL;
|
||||
@ -34,7 +34,9 @@ type
|
||||
private
|
||||
{ Private declarations }
|
||||
public
|
||||
constructor Create;
|
||||
function SearchProducts(const SearchText: string): TDataSet;
|
||||
|
||||
end;
|
||||
|
||||
implementation
|
||||
@ -52,6 +54,11 @@ begin
|
||||
Connection.Params.Values['Database'] := dotEnv.Env('database.path');
|
||||
end;
|
||||
|
||||
constructor TdmMain.Create;
|
||||
begin
|
||||
inherited Create(nil);
|
||||
end;
|
||||
|
||||
function TdmMain.SearchProducts(const SearchText: string): TDataSet;
|
||||
begin
|
||||
Result := TFDMemTable.Create(nil);
|
||||
|
@ -5,23 +5,24 @@ interface
|
||||
uses
|
||||
System.Generics.Collections,
|
||||
BusinessObjects,
|
||||
MainDM,
|
||||
System.SysUtils,
|
||||
Commons, JsonDataObjects;
|
||||
|
||||
type
|
||||
|
||||
TServiceBase = class abstract
|
||||
strict protected
|
||||
FDM: TdmMain;
|
||||
public
|
||||
constructor Create(AdmMain: TdmMain); virtual;
|
||||
procedure Commit;
|
||||
procedure Rollback;
|
||||
procedure StartTransaction;
|
||||
IArticlesService = interface
|
||||
['{D6843E17-1B98-435C-9EBC-8E76DCEF9A3B}']
|
||||
function GetAll: TObjectList<TArticle>;
|
||||
function GetArticles(const aTextSearch: string): TObjectList<TArticle>;
|
||||
function GetByID(const AID: Integer): TArticle;
|
||||
procedure Delete(AArticolo: TArticle);
|
||||
procedure DeleteAllArticles;
|
||||
procedure Add(AArticolo: TArticle);
|
||||
procedure CreateArticles(const ArticleList: TObjectList<TArticle>);
|
||||
procedure Update(AArticolo: TArticle);
|
||||
function GetMeta: TJSONObject;
|
||||
end;
|
||||
|
||||
TArticlesService = class(TServiceBase)
|
||||
TArticlesService = class(TInterfacedObject, IArticlesService)
|
||||
public
|
||||
function GetAll: TObjectList<TArticle>;
|
||||
function GetArticles(const aTextSearch: string): TObjectList<TArticle>;
|
||||
@ -29,6 +30,7 @@ type
|
||||
procedure Delete(AArticolo: TArticle);
|
||||
procedure DeleteAllArticles;
|
||||
procedure Add(AArticolo: TArticle);
|
||||
procedure CreateArticles(const ArticleList: TObjectList<TArticle>);
|
||||
procedure Update(AArticolo: TArticle);
|
||||
function GetMeta: TJSONObject;
|
||||
end;
|
||||
@ -36,112 +38,71 @@ type
|
||||
implementation
|
||||
|
||||
uses
|
||||
FireDAC.Stan.Option,
|
||||
FireDAC.Comp.Client,
|
||||
FireDAC.Stan.Param,
|
||||
MVCFramework.ActiveRecord,
|
||||
MVCFramework.FireDAC.Utils,
|
||||
MVCFramework.DataSet.Utils,
|
||||
MVCFramework.Serializer.Commons;
|
||||
MVCFramework.Serializer.Commons, Data.DB;
|
||||
|
||||
|
||||
{ TArticoliService }
|
||||
|
||||
procedure TArticlesService.Add(AArticolo: TArticle);
|
||||
var
|
||||
Cmd: TFDCustomCommand;
|
||||
begin
|
||||
AArticolo.CheckInsert;
|
||||
Cmd := FDM.updArticles.Commands[arInsert];
|
||||
TFireDACUtils.ObjectToParameters(Cmd.Params, AArticolo, 'NEW_');
|
||||
Cmd.Execute;
|
||||
AArticolo.ID := Cmd.ParamByName('ID').AsInteger;
|
||||
AArticolo.Insert;
|
||||
end;
|
||||
|
||||
procedure TArticlesService.CreateArticles(const ArticleList: TObjectList<TArticle>);
|
||||
begin
|
||||
var Ctx := TMVCActiveRecord.UseTransactionContext;
|
||||
for var lArticle in ArticleList do
|
||||
begin
|
||||
Add(lArticle);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TArticlesService.Delete(AArticolo: TArticle);
|
||||
var
|
||||
Cmd: TFDCustomCommand;
|
||||
begin
|
||||
AArticolo.CheckDelete;
|
||||
Cmd := FDM.updArticles.Commands[arDelete];
|
||||
TFireDACUtils.ObjectToParameters(Cmd.Params, AArticolo, 'OLD_');
|
||||
Cmd.Execute;
|
||||
AArticolo.Delete();
|
||||
end;
|
||||
|
||||
procedure TArticlesService.DeleteAllArticles;
|
||||
begin
|
||||
FDM.Connection.ExecSQL('delete from articoli');
|
||||
TMVCActiveRecord.DeleteAll(TArticle);
|
||||
end;
|
||||
|
||||
function TArticlesService.GetAll: TObjectList<TArticle>;
|
||||
begin
|
||||
FDM.dsArticles.Open('SELECT * FROM ARTICOLI ORDER BY ID', []);
|
||||
Result := FDM.dsArticles.AsObjectList<TArticle>;
|
||||
FDM.dsArticles.Close;
|
||||
Result := TMVCActiveRecord.SelectRQL<TArticle>('sort(+id)', 1000);
|
||||
end;
|
||||
|
||||
function TArticlesService.GetArticles(
|
||||
const aTextSearch: string): TObjectList<TArticle>;
|
||||
begin
|
||||
FDM.dsArticles.Open('SELECT * FROM ARTICOLI WHERE DESCRIZIONE CONTAINING ? ORDER BY ID', [aTextSearch]);
|
||||
try
|
||||
Result := FDM.dsArticles.AsObjectList<TArticle>()
|
||||
finally
|
||||
FDM.dsArticles.Close;
|
||||
end;
|
||||
if aTextSearch.Trim.IsEmpty then
|
||||
Result := GetAll
|
||||
else
|
||||
Result := TMVCActiveRecord.SelectByNamedQuery<TArticle>('search_by_text',[aTextSearch],[ftString]);
|
||||
end;
|
||||
|
||||
function TArticlesService.GetByID(const AID: Integer): TArticle;
|
||||
begin
|
||||
FDM.dsArticles.Open('SELECT * FROM ARTICOLI WHERE ID = :ID', [AID]);
|
||||
try
|
||||
if not FDM.dsArticles.Eof then
|
||||
Result := FDM.dsArticles.AsObject<TArticle>
|
||||
else
|
||||
raise EServiceException.Create('Article not found');
|
||||
finally
|
||||
FDM.dsArticles.Close;
|
||||
end;
|
||||
Result := TMVCActiveRecord.GetByPK<TArticle>(AID);
|
||||
end;
|
||||
|
||||
function TArticlesService.GetMeta: TJSONObject;
|
||||
begin
|
||||
FDM.dsArticles.Open('SELECT ID, CODICE as CODE, DESCRIZIONE as DESCRIPTION, PREZZO as PRICE, CREATED_AT as CREATEDAT, UPDATED_AT as UPDATEDAT FROM ARTICOLI WHERE TRUE = FALSE');
|
||||
Result := FDM.dsArticles.MetadataAsJSONObject();
|
||||
var lDS := TMVCActiveRecord.SelectDataSet('SELECT ID, CODICE as CODE, DESCRIZIONE as DESCRIPTION, PREZZO as PRICE, CREATED_AT as CREATEDAT, UPDATED_AT as UPDATEDAT FROM ARTICOLI WHERE TRUE = FALSE',[]);
|
||||
try
|
||||
Result := lDS.MetadataAsJSONObject()
|
||||
finally
|
||||
lDS.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TArticlesService.Update(AArticolo: TArticle);
|
||||
var
|
||||
Cmd: TFDCustomCommand;
|
||||
begin
|
||||
AArticolo.CheckUpdate;
|
||||
Cmd := FDM.updArticles.Commands[arUpdate];
|
||||
TFireDACUtils.ObjectToParameters(Cmd.Params, AArticolo, 'NEW_');
|
||||
Cmd.ParamByName('OLD_ID').AsInteger := AArticolo.ID;
|
||||
Cmd.Execute;
|
||||
if Cmd.RowsAffected <> 1 then
|
||||
raise Exception.Create('Article not found');
|
||||
end;
|
||||
|
||||
{ TServiceBase }
|
||||
|
||||
procedure TServiceBase.Commit;
|
||||
begin
|
||||
FDM.Connection.Commit;
|
||||
end;
|
||||
|
||||
constructor TServiceBase.Create(AdmMain: TdmMain);
|
||||
begin
|
||||
inherited Create;
|
||||
FDM := AdmMain;
|
||||
end;
|
||||
|
||||
procedure TServiceBase.Rollback;
|
||||
begin
|
||||
FDM.Connection.Rollback;
|
||||
end;
|
||||
|
||||
procedure TServiceBase.StartTransaction;
|
||||
begin
|
||||
FDM.Connection.StartTransaction;
|
||||
AArticolo.Update();
|
||||
end;
|
||||
|
||||
end.
|
||||
|
@ -2,7 +2,8 @@ unit WebModuleUnit1;
|
||||
|
||||
interface
|
||||
|
||||
uses System.SysUtils, System.Classes, Web.HTTPApp, mvcframework;
|
||||
uses System.SysUtils, System.Classes, Web.HTTPApp, mvcframework, FireDAC.Phys.FBDef, FireDAC.Stan.Intf, FireDAC.Phys,
|
||||
FireDAC.Phys.IBBase, FireDAC.Phys.FB;
|
||||
|
||||
type
|
||||
TWebModule1 = class(TWebModule)
|
||||
@ -23,10 +24,13 @@ implementation
|
||||
uses
|
||||
Controllers.Articles,
|
||||
MVCFramework.Middleware.CORS,
|
||||
MVCFramework.Middleware.ActiveRecord,
|
||||
MVCFramework.Middleware.Compression,
|
||||
MVCFramework.Middleware.Trace,
|
||||
MVCFramework.SQLGenerators.Firebird,
|
||||
MVCFramework.Commons,
|
||||
Controllers.Base;
|
||||
Controllers.Base,
|
||||
Commons;
|
||||
|
||||
{$R *.dfm}
|
||||
|
||||
@ -45,6 +49,8 @@ begin
|
||||
{$ENDIF}
|
||||
FEngine.AddMiddleware(TCORSMiddleware.Create);
|
||||
FEngine.AddMiddleware(TMVCCompressionMiddleware.Create(256));
|
||||
FEngine.AddMiddleware(TMVCActiveRecordMiddleware.Create(CON_DEF_NAME));
|
||||
|
||||
// FEngine.AddMiddleware(TMVCTraceMiddleware.Create);
|
||||
end;
|
||||
|
||||
|
@ -10,6 +10,7 @@ uses
|
||||
MVCFramework.Commons,
|
||||
MVCFramework.Signal,
|
||||
MVCFramework.Logger,
|
||||
MVCFramework.Container,
|
||||
MVCFramework.dotEnv,
|
||||
Web.WebReq,
|
||||
Web.WebBroker,
|
||||
@ -18,10 +19,10 @@ uses
|
||||
Controllers.Articles in 'Controllers.Articles.pas',
|
||||
Services in 'Services.pas',
|
||||
BusinessObjects in 'BusinessObjects.pas',
|
||||
MainDM in 'MainDM.pas' {dmMain: TDataModule},
|
||||
Commons in 'Commons.pas',
|
||||
MVCFramework.ActiveRecord in '..\..\sources\MVCFramework.ActiveRecord.pas',
|
||||
MVCFramework.Serializer.JsonDataObjects in '..\..\sources\MVCFramework.Serializer.JsonDataObjects.pas';
|
||||
MVCFramework.Serializer.JsonDataObjects in '..\..\sources\MVCFramework.Serializer.JsonDataObjects.pas',
|
||||
FDConnectionConfigU in 'FDConnectionConfigU.pas';
|
||||
|
||||
{$R *.res}
|
||||
|
||||
@ -68,6 +69,12 @@ begin
|
||||
.Build(); //uses the executable folder to look for .env* files
|
||||
end);
|
||||
|
||||
CreateFirebirdPrivateConnDef(True);
|
||||
|
||||
DefaultMVCServiceContainer
|
||||
.RegisterType(TArticlesService, IArticlesService, '', TRegistrationType.SingletonPerRequest)
|
||||
.Build;
|
||||
|
||||
WebRequestHandlerProc.MaxConnections := dotEnv.Env('dmvc.handler.max_connections', 1024);
|
||||
RunServer(dotEnv.Env('dmvc.server.port', 8080));
|
||||
except
|
||||
|
@ -97,13 +97,10 @@
|
||||
<DCCReference Include="Controllers.Articles.pas"/>
|
||||
<DCCReference Include="Services.pas"/>
|
||||
<DCCReference Include="BusinessObjects.pas"/>
|
||||
<DCCReference Include="MainDM.pas">
|
||||
<Form>dmMain</Form>
|
||||
<DesignClass>TDataModule</DesignClass>
|
||||
</DCCReference>
|
||||
<DCCReference Include="Commons.pas"/>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.ActiveRecord.pas"/>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.Serializer.JsonDataObjects.pas"/>
|
||||
<DCCReference Include="FDConnectionConfigU.pas"/>
|
||||
<BuildConfiguration Include="Base">
|
||||
<Key>Base</Key>
|
||||
</BuildConfiguration>
|
||||
|
@ -8,7 +8,7 @@ uses
|
||||
FireDAC.Stan.Param, FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf,
|
||||
FireDAC.DApt.Intf, Data.DB, FireDAC.Comp.DataSet, FireDAC.Comp.Client,
|
||||
Vcl.Grids, Vcl.DBGrids, Vcl.ExtCtrls, Vcl.StdCtrls, MVCFramework.RESTClient.Intf, MVCFramework.RESTClient,
|
||||
Vcl.DBCtrls, MVCFramework.DataSet.Utils;
|
||||
Vcl.DBCtrls, MVCFramework.DataSet.Utils, Vcl.Buttons;
|
||||
|
||||
type
|
||||
TMainForm = class(TForm)
|
||||
@ -38,11 +38,8 @@ type
|
||||
procedure btnFilterClick(Sender: TObject);
|
||||
private
|
||||
fFilter: string;
|
||||
fLoading: Boolean;
|
||||
fRESTClient: IMVCRESTClient;
|
||||
fAPIBinder: TMVCAPIBinder;
|
||||
{ Private declarations }
|
||||
procedure ShowError(const AResponse: IMVCRESTResponse);
|
||||
procedure SetFilter(const Value: string);
|
||||
public
|
||||
property Filter: string read fFilter write SetFilter;
|
||||
@ -102,18 +99,4 @@ begin
|
||||
EditFilter.Text := Value;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ShowError(const AResponse: IMVCRESTResponse);
|
||||
begin
|
||||
if not AResponse.Success then
|
||||
MessageDlg(
|
||||
AResponse.StatusCode.ToString + ': ' + AResponse.StatusText + sLineBreak +
|
||||
'[' + AResponse.Content + ']',
|
||||
mtError, [mbOK], 0)
|
||||
else
|
||||
MessageDlg(
|
||||
AResponse.StatusCode.ToString + ': ' + AResponse.StatusText + sLineBreak +
|
||||
AResponse.Content,
|
||||
mtError, [mbOK], 0);
|
||||
end;
|
||||
|
||||
end.
|
||||
|
345
samples/commons/Entities.pas
Normal file
345
samples/commons/Entities.pas
Normal file
@ -0,0 +1,345 @@
|
||||
unit Entities;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
MVCFramework.Serializer.Commons,
|
||||
MVCFramework.ActiveRecord,
|
||||
MVCFramework.Nullables,
|
||||
System.Classes,
|
||||
System.DateUtils,
|
||||
MVCFramework,
|
||||
MVCFramework.Utils,
|
||||
System.Generics.Collections;
|
||||
|
||||
type
|
||||
|
||||
[MVCNameCase(ncCamelCase)]
|
||||
[MVCTable('people')]
|
||||
[MVCEntityActions([eaCreate, eaRetrieve, eaUpdate, eaDelete])]
|
||||
TPerson = class(TMVCActiveRecord)
|
||||
private
|
||||
[MVCTableField('id', [foPrimaryKey, foAutoGenerated])]
|
||||
fID: Int64;
|
||||
[MVCTableField('LAST_NAME')]
|
||||
fLastName: string;
|
||||
[MVCTableField('FIRST_NAME')]
|
||||
fFirstName: string;
|
||||
[MVCTableField('DOB')]
|
||||
fDOB: NullableTDate;
|
||||
[MVCTableField('FULL_NAME')]
|
||||
fFullName: string;
|
||||
[MVCTableField('IS_MALE')]
|
||||
fIsMale: NullableBoolean;
|
||||
[MVCTableField('NOTE')]
|
||||
fNote: string;
|
||||
[MVCTableField('PHOTO')]
|
||||
fPhoto: TStream;
|
||||
|
||||
// transient fields
|
||||
fAge: NullableInt32;
|
||||
|
||||
procedure SetLastName(const Value: string);
|
||||
procedure SetID(const Value: Int64);
|
||||
procedure SetFirstName(const Value: string);
|
||||
procedure SetDOB(const Value: NullableTDate);
|
||||
function GetFullName: string;
|
||||
procedure SetIsMale(const Value: NullableBoolean);
|
||||
procedure SetNote(const Value: string);
|
||||
protected
|
||||
procedure OnAfterLoad; override;
|
||||
procedure OnBeforeInsertOrUpdate; override;
|
||||
procedure OnValidation(const Action: TMVCEntityAction); override;
|
||||
procedure OnBeforeInsert; override;
|
||||
public
|
||||
constructor Create; override;
|
||||
destructor Destroy; override;
|
||||
function GetUniqueString: String;
|
||||
procedure Assign(ActiveRecord: TMVCActiveRecord); override;
|
||||
property ID: Int64 read fID write SetID;
|
||||
[MVCNameAs('person_surname')]
|
||||
property LastName: string read fLastName write SetLastName;
|
||||
[MVCNameAs('person_name')]
|
||||
property FirstName: string read fFirstName write SetFirstName;
|
||||
property Age: NullableInt32 read fAge;
|
||||
property DOB: NullableTDate read fDOB write SetDOB;
|
||||
property FullName: string read GetFullName;
|
||||
property IsMale: NullableBoolean read fIsMale write SetIsMale;
|
||||
property Note: string read fNote write SetNote;
|
||||
property Photo: TStream read fPhoto;
|
||||
end;
|
||||
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
[MVCTable('phones')]
|
||||
[MVCEntityActions([eaCreate, eaRetrieve, eaUpdate, eaDelete])]
|
||||
TPhone = class(TMVCActiveRecord)
|
||||
private
|
||||
[MVCTableField('id', [foPrimaryKey, foAutoGenerated])]
|
||||
fID: Integer;
|
||||
[MVCTableField('phone_number')]
|
||||
fPhoneNumber: string;
|
||||
[MVCTableField('number_type')]
|
||||
fNumberType: string;
|
||||
[MVCTableField('id_person')]
|
||||
fIDPerson: Integer;
|
||||
protected
|
||||
procedure OnValidation(const Action: TMVCEntityAction); override;
|
||||
public
|
||||
property ID: Integer read fID write fID;
|
||||
property IDPerson: Integer read fIDPerson write fIDPerson;
|
||||
property PhoneNumber: string read fPhoneNumber write fPhoneNumber;
|
||||
property NumberType: string read fNumberType write fNumberType;
|
||||
end;
|
||||
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
[MVCTable('PEOPLE')]
|
||||
[MVCEntityActions([eaCreate, eaRetrieve, eaUpdate, eaDelete])]
|
||||
TContact = class(TPerson)
|
||||
private
|
||||
function GetPhones: TObjectList<TPhone>;
|
||||
public
|
||||
property Phones: TObjectList<TPhone> read GetPhones;
|
||||
end;
|
||||
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
[MVCEntityActions([eaRetrieve])]
|
||||
[MVCNamedSQLQuery('AverageSalary',
|
||||
'select person_type, coalesce(avg(salary::numeric), 0) average_salary from people ' +
|
||||
'group by person_type order by 1', TMVCActiveRecordBackEnd.PostgreSQL)]
|
||||
TSalaryAggregate = class(TMVCActiveRecord)
|
||||
private
|
||||
[MVCTableField('average_salary')]
|
||||
FAverageSalary: Currency;
|
||||
[MVCTableField('person_type')]
|
||||
FPersonType: String;
|
||||
procedure SetAverageSalary(const Value: Currency);
|
||||
procedure SetPersonType(const Value: String);
|
||||
public
|
||||
property PersonType: String read FPersonType write SetPersonType;
|
||||
property AverageSalary: Currency read FAverageSalary write SetAverageSalary;
|
||||
end;
|
||||
|
||||
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
[MVCTable('articles')]
|
||||
[MVCEntityActions([eaCreate, eaRetrieve, eaUpdate, eaDelete])]
|
||||
TArticle = class(TMVCActiveRecord)
|
||||
private
|
||||
[MVCTableField('id', [foPrimaryKey, foAutoGenerated])]
|
||||
fID: Int64;
|
||||
[MVCTableField('price')]
|
||||
FPrice: UInt32;
|
||||
[MVCTableField('description')]
|
||||
FDescription: string;
|
||||
procedure SetID(const Value: Int64);
|
||||
procedure SetDescription(const Value: string);
|
||||
procedure SetPrice(const Value: UInt32);
|
||||
public
|
||||
property ID: Int64 read fID write SetID;
|
||||
property Description: string read FDescription write SetDescription;
|
||||
property Price: UInt32 read FPrice write SetPrice;
|
||||
end;
|
||||
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
[MVCTable('customers_with_version')]
|
||||
TCustomersWithVersion = class(TMVCActiveRecord)
|
||||
private
|
||||
[MVCTableField('id', [foPrimaryKey, foAutoGenerated])]
|
||||
fID: Int64;
|
||||
[MVCTableField('code')]
|
||||
fCode: NullableString;
|
||||
[MVCTableField('description')]
|
||||
fDescription: NullableString;
|
||||
[MVCTableField('city')]
|
||||
fCity: NullableString;
|
||||
[MVCTableField('note')]
|
||||
fNote: NullableString;
|
||||
[MVCTableField('rating')]
|
||||
fRating: NullableInt32;
|
||||
[MVCTableField('objversion', [foVersion])]
|
||||
fObjversion: Integer;
|
||||
public
|
||||
property ID: Int64 read fID write fID;
|
||||
property Code: NullableString read fCode write fCode;
|
||||
property Description: NullableString read fDescription write fDescription;
|
||||
property City: NullableString read fCity write fCity;
|
||||
property Note: NullableString read fNote write fNote;
|
||||
property Rating: NullableInt32 read fRating write fRating;
|
||||
property ObjVersion: Integer read fObjversion write fObjversion;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.SysUtils;
|
||||
|
||||
{ TPersona }
|
||||
|
||||
procedure TPerson.Assign(ActiveRecord: TMVCActiveRecord);
|
||||
begin
|
||||
if ActiveRecord is TPerson then
|
||||
begin
|
||||
var lPerson := TPerson(ActiveRecord);
|
||||
Self.LastName := lPerson.LastName;
|
||||
Self.FirstName := lPerson.FirstName;
|
||||
Self.DOB := lPerson.DOB;
|
||||
Self.IsMale := lPerson.IsMale;
|
||||
Self.Note := lPerson.Note;
|
||||
Self.Photo.Size := 0;
|
||||
Self.Photo.CopyFrom(lPerson.Photo);
|
||||
Self.Photo.Position := 0;
|
||||
end
|
||||
else
|
||||
inherited;
|
||||
end;
|
||||
|
||||
constructor TPerson.Create;
|
||||
begin
|
||||
inherited;
|
||||
fPhoto := TMemoryStream.Create;
|
||||
end;
|
||||
|
||||
destructor TPerson.Destroy;
|
||||
begin
|
||||
fPhoto.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
function TPerson.GetFullName: string;
|
||||
begin
|
||||
Result := fFullName;
|
||||
end;
|
||||
|
||||
function TPerson.GetUniqueString: String;
|
||||
begin
|
||||
Result :=
|
||||
fID.ToString + '|' +
|
||||
fFirstName + '|' +
|
||||
fLastName + '|' +
|
||||
DateToISODate(fDOB.ValueOrDefault) + '|' +
|
||||
BoolToStr(fIsMale.ValueOrDefault, True) + '|' +
|
||||
GetSHA1HashFromStream(fPhoto);
|
||||
end;
|
||||
|
||||
procedure TPerson.OnAfterLoad;
|
||||
begin
|
||||
inherited;
|
||||
if fDOB.HasValue then
|
||||
begin
|
||||
fAge := YearsBetween(fDOB, now);
|
||||
end
|
||||
else
|
||||
begin
|
||||
fAge.Clear;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPerson.OnBeforeInsert;
|
||||
begin
|
||||
inherited;
|
||||
end;
|
||||
|
||||
procedure TPerson.OnBeforeInsertOrUpdate;
|
||||
begin
|
||||
inherited;
|
||||
fLastName := fLastName.ToUpper;
|
||||
fFirstName := fFirstName.ToUpper;
|
||||
fFullName := fFirstName + ' ' + fLastName;
|
||||
end;
|
||||
|
||||
procedure TPerson.OnValidation(const Action: TMVCEntityAction);
|
||||
begin
|
||||
inherited;
|
||||
if fLastName.Trim.IsEmpty or fFirstName.Trim.IsEmpty then
|
||||
raise EMVCActiveRecord.Create
|
||||
('Validation error. FirstName and LastName are required');
|
||||
end;
|
||||
|
||||
procedure TPerson.SetLastName(const Value: string);
|
||||
begin
|
||||
fLastName := Value;
|
||||
end;
|
||||
|
||||
procedure TPerson.SetNote(const Value: string);
|
||||
begin
|
||||
fNote := Value;
|
||||
end;
|
||||
|
||||
procedure TPerson.SetDOB(const Value: NullableTDate);
|
||||
begin
|
||||
fDOB := Value;
|
||||
end;
|
||||
|
||||
procedure TPerson.SetID(const Value: Int64);
|
||||
begin
|
||||
fID := Value;
|
||||
end;
|
||||
|
||||
procedure TPerson.SetIsMale(const Value: NullableBoolean);
|
||||
begin
|
||||
fIsMale := Value;
|
||||
end;
|
||||
|
||||
procedure TPerson.SetFirstName(const Value: string);
|
||||
begin
|
||||
fFirstName := Value;
|
||||
end;
|
||||
|
||||
{ TArticle }
|
||||
|
||||
procedure TArticle.SetDescription(const Value: string);
|
||||
begin
|
||||
FDescription := Value;
|
||||
end;
|
||||
|
||||
procedure TArticle.SetID(const Value: Int64);
|
||||
begin
|
||||
fID := Value;
|
||||
end;
|
||||
|
||||
procedure TArticle.SetPrice(const Value: UInt32);
|
||||
begin
|
||||
FPrice := Value;
|
||||
end;
|
||||
|
||||
{ TPhone }
|
||||
|
||||
procedure TPhone.OnValidation(const Action: TMVCEntityAction);
|
||||
begin
|
||||
inherited;
|
||||
if fPhoneNumber.Trim.IsEmpty then
|
||||
raise EMVCActiveRecord.Create('Phone Number cannot be empty');
|
||||
end;
|
||||
|
||||
{ TContact }
|
||||
|
||||
function TContact.GetPhones: TObjectList<TPhone>;
|
||||
begin
|
||||
Result := TMVCActiveRecord.SelectRQL<TPhone>('eq(IDPerson, ' +
|
||||
self.ID.ToString + ')', 100);
|
||||
end;
|
||||
|
||||
{ TSalaryAggregate }
|
||||
|
||||
procedure TSalaryAggregate.SetAverageSalary(const Value: Currency);
|
||||
begin
|
||||
FAverageSalary := Value;
|
||||
end;
|
||||
|
||||
procedure TSalaryAggregate.SetPersonType(const Value: String);
|
||||
begin
|
||||
FPersonType := Value;
|
||||
end;
|
||||
|
||||
initialization
|
||||
|
||||
ActiveRecordMappingRegistry.AddEntity('people', TPerson);
|
||||
ActiveRecordMappingRegistry.AddEntity('salary', TSalaryAggregate);
|
||||
ActiveRecordMappingRegistry.AddEntity('contacts', TContact);
|
||||
ActiveRecordMappingRegistry.AddEntity('phones', TPhone);
|
||||
ActiveRecordMappingRegistry.AddEntity('articles', TArticle);
|
||||
ActiveRecordMappingRegistry.AddEntity('customers', TCustomersWithVersion);
|
||||
|
||||
finalization
|
||||
|
||||
end.
|
@ -82,6 +82,11 @@ type
|
||||
function GetMVCResponseWithObjectDictionary: IMVCResponse;
|
||||
[MVCPath('/mvcresponse/message/builder/headers')]
|
||||
function GetMVCResponseSimpleBuilderWithHeaders: IMVCResponse;
|
||||
[MVCPath('/mvcresponse/message/builder/nobody')]
|
||||
function GetMVCResponseNoBody: IMVCResponse;
|
||||
// Standard Responses
|
||||
[MVCPath('/mvcresponse/ok')]
|
||||
function GetOKResponse: IMVCResponse;
|
||||
end;
|
||||
|
||||
implementation
|
||||
@ -132,6 +137,15 @@ end;
|
||||
|
||||
|
||||
|
||||
function TMyController.GetMVCResponseNoBody: IMVCResponse;
|
||||
begin
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.OK)
|
||||
.Header('header1', 'Hello World')
|
||||
.Header('header2', 'foo bar')
|
||||
.Build;
|
||||
end;
|
||||
|
||||
function TMyController.GetMVCResponseSimple: IMVCResponse;
|
||||
begin
|
||||
Result := MVCResponseBuilder
|
||||
@ -152,10 +166,7 @@ end;
|
||||
|
||||
function TMyController.GetMVCResponseWithData: IMVCResponse;
|
||||
begin
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.OK)
|
||||
.Body(TPerson.Create('Daniele','Teti', 99))
|
||||
.Build;
|
||||
Result := OKResponse(TPerson.Create('Daniele','Teti', 99));
|
||||
end;
|
||||
|
||||
function TMyController.GetMVCResponseWithDataAndMessage: IMVCResponse;
|
||||
@ -191,22 +202,21 @@ end;
|
||||
|
||||
function TMyController.GetMVCResponseWithObjectList: IMVCResponse;
|
||||
begin
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.OK)
|
||||
.Body(TObjectList<TPerson>.Create([
|
||||
TPerson.Create('Daniele','Teti', 99),
|
||||
TPerson.Create('Peter','Parker', 25),
|
||||
TPerson.Create('Bruce','Banner', 45)
|
||||
])
|
||||
).Build;
|
||||
Result := OKResponse(TObjectList<TPerson>.Create([
|
||||
TPerson.Create('Daniele','Teti', 99),
|
||||
TPerson.Create('Peter','Parker', 25),
|
||||
TPerson.Create('Bruce','Banner', 45)
|
||||
]));
|
||||
end;
|
||||
|
||||
function TMyController.GetOKResponse: IMVCResponse;
|
||||
begin
|
||||
Result := OKResponse;
|
||||
end;
|
||||
|
||||
function TMyController.GetMVCResponseWithJSON: IMVCResponse;
|
||||
begin
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.OK)
|
||||
.Body(StrToJSONObject('{"name":"Daniele","surname":"Teti"}'))
|
||||
.Build;
|
||||
Result := OKResponse(StrToJSONObject('{"name":"Daniele","surname":"Teti"}'));
|
||||
end;
|
||||
|
||||
function TMyController.GetSingleObject: TPerson;
|
||||
|
116
samples/services_injection/MainControllerU.pas
Normal file
116
samples/services_injection/MainControllerU.pas
Normal file
@ -0,0 +1,116 @@
|
||||
unit MainControllerU;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
MVCFramework, MVCFramework.Commons, MVCFramework.Serializer.Commons,
|
||||
System.Generics.Collections, Services.InterfacesU,
|
||||
Entities, MVCFramework.Container;
|
||||
|
||||
type
|
||||
[MVCPath('/api')]
|
||||
TMyController = class(TMVCController)
|
||||
private
|
||||
fPeopleService: IPeopleService;
|
||||
public
|
||||
[MVCInject]
|
||||
constructor Create(const PeopleService: IPeopleService); reintroduce;
|
||||
|
||||
[MVCPath('/people')]
|
||||
[MVCHTTPMethod([httpGET])]
|
||||
function GetPeople: TObjectList<TPerson>;
|
||||
|
||||
[MVCPath('/people2')]
|
||||
[MVCHTTPMethod([httpGET])]
|
||||
function GetPeople2([MVCInject] OtherPeopleService: IPeopleService): TObjectList<TPerson>;
|
||||
|
||||
[MVCPath('/people/($ID)')]
|
||||
[MVCHTTPMethod([httpGET])]
|
||||
function GetPerson(ID: Integer): TPerson;
|
||||
|
||||
[MVCPath('/people')]
|
||||
[MVCHTTPMethod([httpPOST])]
|
||||
function CreatePerson([MVCFromBody] Person: TPerson): IMVCResponse;
|
||||
|
||||
[MVCPath('/people/($ID)')]
|
||||
[MVCHTTPMethod([httpPUT])]
|
||||
function UpdatePerson(ID: Integer; [MVCFromBody] Person: TPerson): IMVCResponse;
|
||||
|
||||
[MVCPath('/people/($ID)')]
|
||||
[MVCHTTPMethod([httpDELETE])]
|
||||
function DeletePerson(ID: Integer): IMVCResponse;
|
||||
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.SysUtils, MVCFramework.Logger, System.StrUtils;
|
||||
|
||||
|
||||
|
||||
//Sample CRUD Actions for a "People" entity
|
||||
function TMyController.GetPeople: TObjectList<TPerson>;
|
||||
begin
|
||||
Result := fPeopleService.GetAll;
|
||||
end;
|
||||
|
||||
function TMyController.GetPeople2(OtherPeopleService: IPeopleService): TObjectList<TPerson>;
|
||||
begin
|
||||
LogI('PeopleService in GetPeople2: ' + IntToHex(NativeUInt(Pointer(OtherPeopleService))));
|
||||
Result := OtherPeopleService.GetAll;
|
||||
end;
|
||||
|
||||
function TMyController.GetPerson(ID: Integer): TPerson;
|
||||
var
|
||||
lPeople: TObjectList<TPerson>;
|
||||
begin
|
||||
lPeople := GetPeople;
|
||||
try
|
||||
Result := lPeople.ExtractAt(ID mod lPeople.Count);
|
||||
finally
|
||||
lPeople.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
//constructor TMyController.Create;
|
||||
//begin
|
||||
// inherited Create;
|
||||
//end;
|
||||
|
||||
constructor TMyController.Create(const PeopleService: IPeopleService);
|
||||
begin
|
||||
inherited Create;
|
||||
Assert(PeopleService <> nil, 'PeopleService not injected');
|
||||
fPeopleService := PeopleService;
|
||||
LogI('PeopleService in constructor: ' + IntToHex(NativeUInt(Pointer(PeopleService))));
|
||||
end;
|
||||
|
||||
|
||||
function TMyController.CreatePerson([MVCFromBody] Person: TPerson): IMVCResponse;
|
||||
begin
|
||||
LogI('Created ' + Person.FirstName + ' ' + Person.LastName);
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.Created)
|
||||
.Body('Person created')
|
||||
.Build;
|
||||
end;
|
||||
|
||||
function TMyController.UpdatePerson(ID: Integer; [MVCFromBody] Person: TPerson): IMVCResponse;
|
||||
begin
|
||||
LogI('Updated ' + Person.FirstName + ' ' + Person.LastName);
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.NoContent)
|
||||
.Build;
|
||||
end;
|
||||
|
||||
function TMyController.DeletePerson(ID: Integer): IMVCResponse;
|
||||
begin
|
||||
LogI('Deleted person with id ' + ID.ToString);
|
||||
Result := MVCResponseBuilder
|
||||
.StatusCode(HTTP_STATUS.NoContent)
|
||||
.Build;
|
||||
end;
|
||||
|
||||
|
||||
end.
|
40
samples/services_injection/Services.ConnectionU.pas
Normal file
40
samples/services_injection/Services.ConnectionU.pas
Normal file
@ -0,0 +1,40 @@
|
||||
unit Services.ConnectionU;
|
||||
|
||||
interface
|
||||
|
||||
uses Services.InterfacesU;
|
||||
|
||||
type
|
||||
TConnectionService = class(TInterfacedObject, IConnectionService)
|
||||
protected
|
||||
function GetConnectionName: string;
|
||||
public
|
||||
constructor Create; virtual;
|
||||
destructor Destroy; override;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
MVCFramework.Logger, System.SysUtils;
|
||||
|
||||
{ TConnectionService }
|
||||
|
||||
constructor TConnectionService.Create;
|
||||
begin
|
||||
inherited;
|
||||
LogI('Service ' + ClassName + ' created [' + IntToHex(NativeUInt(Pointer(Self))) + ']');
|
||||
end;
|
||||
|
||||
destructor TConnectionService.Destroy;
|
||||
begin
|
||||
LogI('Service ' + ClassName + ' destroyed [' + IntToHex(NativeUInt(Pointer(Self))) + ']');
|
||||
inherited;
|
||||
end;
|
||||
|
||||
function TConnectionService.GetConnectionName: string;
|
||||
begin
|
||||
Result := 'MyDemoConnection';
|
||||
end;
|
||||
|
||||
end.
|
21
samples/services_injection/Services.InterfacesU.pas
Normal file
21
samples/services_injection/Services.InterfacesU.pas
Normal file
@ -0,0 +1,21 @@
|
||||
unit Services.InterfacesU;
|
||||
|
||||
interface
|
||||
|
||||
uses Entities, System.Generics.Collections;
|
||||
|
||||
|
||||
type
|
||||
IPeopleService = interface
|
||||
['{347532A0-1B28-40C3-A2E9-51DF62365FE7}']
|
||||
function GetAll: TObjectList<TPerson>;
|
||||
end;
|
||||
|
||||
IConnectionService = interface
|
||||
['{146C21A5-07E8-456D-8E6D-A72820BD17AA}']
|
||||
function GetConnectionName: String;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
end.
|
51
samples/services_injection/Services.PeopleU.pas
Normal file
51
samples/services_injection/Services.PeopleU.pas
Normal file
@ -0,0 +1,51 @@
|
||||
unit Services.PeopleU;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.Generics.Collections, Entities, Services.InterfacesU, MVCFramework.Logger,
|
||||
MVCFramework, MVCFramework.Container;
|
||||
|
||||
type
|
||||
TPeopleService = class(TInterfacedObject, IPeopleService)
|
||||
private
|
||||
fConnService: IConnectionService;
|
||||
public
|
||||
[MVCInject]
|
||||
constructor Create(ConnectionService: IConnectionService); virtual;
|
||||
destructor Destroy; override;
|
||||
function GetAll: TObjectList<TPerson>;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.SysUtils {IntToHex};
|
||||
|
||||
{ TPeopleService }
|
||||
|
||||
constructor TPeopleService.Create(ConnectionService: IConnectionService);
|
||||
begin
|
||||
inherited Create;
|
||||
fConnService := ConnectionService;
|
||||
LogI('Service ' + ClassName + ' created [' + IntToHex(NativeUInt(Pointer(Self))) + ']');
|
||||
end;
|
||||
|
||||
destructor TPeopleService.Destroy;
|
||||
begin
|
||||
LogI('Service ' + ClassName + ' destroyed [' + IntToHex(NativeUInt(Pointer(Self))) + ']');
|
||||
inherited;
|
||||
end;
|
||||
|
||||
function TPeopleService.GetAll: TObjectList<TPerson>;
|
||||
begin
|
||||
Result := TObjectList<TPerson>.Create;
|
||||
Result.AddRange([
|
||||
TPerson.Create,
|
||||
TPerson.Create,
|
||||
TPerson.Create,
|
||||
TPerson.Create
|
||||
]);
|
||||
end;
|
||||
|
||||
end.
|
21
samples/services_injection/Services.RegistrationU.pas
Normal file
21
samples/services_injection/Services.RegistrationU.pas
Normal file
@ -0,0 +1,21 @@
|
||||
unit Services.RegistrationU;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
MVCFramework.Container;
|
||||
|
||||
procedure RegisterServices(Container: IMVCServiceContainer);
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
Services.PeopleU, Services.InterfacesU, Services.ConnectionU;
|
||||
|
||||
procedure RegisterServices(Container: IMVCServiceContainer);
|
||||
begin
|
||||
Container.RegisterType(TPeopleService, IPeopleService);
|
||||
Container.RegisterType(TConnectionService, IConnectionService, '', TRegistrationType.SingletonPerRequest)
|
||||
end;
|
||||
|
||||
end.
|
7
samples/services_injection/WebModuleU.dfm
Normal file
7
samples/services_injection/WebModuleU.dfm
Normal file
@ -0,0 +1,7 @@
|
||||
object MyWebModule: TMyWebModule
|
||||
OnCreate = WebModuleCreate
|
||||
OnDestroy = WebModuleDestroy
|
||||
Actions = <>
|
||||
Height = 230
|
||||
Width = 415
|
||||
end
|
125
samples/services_injection/WebModuleU.pas
Normal file
125
samples/services_injection/WebModuleU.pas
Normal file
@ -0,0 +1,125 @@
|
||||
unit WebModuleU;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.SysUtils,
|
||||
System.Classes,
|
||||
Web.HTTPApp,
|
||||
MVCFramework, Services.PeopleU, Services.InterfacesU;
|
||||
|
||||
type
|
||||
TMyWebModule = class(TWebModule)
|
||||
procedure WebModuleCreate(Sender: TObject);
|
||||
procedure WebModuleDestroy(Sender: TObject);
|
||||
private
|
||||
FMVC: TMVCEngine;
|
||||
public
|
||||
{ Public declarations }
|
||||
end;
|
||||
|
||||
var
|
||||
WebModuleClass: TComponentClass = TMyWebModule;
|
||||
|
||||
implementation
|
||||
|
||||
{$R *.dfm}
|
||||
|
||||
uses
|
||||
MainControllerU,
|
||||
System.IOUtils,
|
||||
MVCFramework.SQLGenerators.PostgreSQL,
|
||||
MVCFramework.Commons,
|
||||
MVCFramework.Middleware.ActiveRecord,
|
||||
MVCFramework.Middleware.StaticFiles,
|
||||
MVCFramework.Middleware.Analytics,
|
||||
MVCFramework.Middleware.Trace,
|
||||
MVCFramework.Middleware.CORS,
|
||||
MVCFramework.Middleware.ETag,
|
||||
MVCFramework.Middleware.Compression;
|
||||
|
||||
procedure TMyWebModule.WebModuleCreate(Sender: TObject);
|
||||
begin
|
||||
FMVC := TMVCEngine.Create(Self,
|
||||
procedure(Config: TMVCConfig)
|
||||
begin
|
||||
Config.dotEnv := dotEnv;
|
||||
// session timeout (0 means session cookie)
|
||||
Config[TMVCConfigKey.SessionTimeout] := dotEnv.Env('dmvc.session_timeout', '0');
|
||||
//default content-type
|
||||
Config[TMVCConfigKey.DefaultContentType] := dotEnv.Env('dmvc.default.content_type', TMVCConstants.DEFAULT_CONTENT_TYPE);
|
||||
//default content charset
|
||||
Config[TMVCConfigKey.DefaultContentCharset] := dotEnv.Env('dmvc.default.content_charset', TMVCConstants.DEFAULT_CONTENT_CHARSET);
|
||||
//unhandled actions are permitted?
|
||||
Config[TMVCConfigKey.AllowUnhandledAction] := dotEnv.Env('dmvc.allow_unhandled_actions', 'false');
|
||||
//enables or not system controllers loading (available only from localhost requests)
|
||||
Config[TMVCConfigKey.LoadSystemControllers] := dotEnv.Env('dmvc.load_system_controllers', 'true');
|
||||
//default view file extension
|
||||
Config[TMVCConfigKey.DefaultViewFileExtension] := dotEnv.Env('dmvc.default.view_file_extension', 'html');
|
||||
//view path
|
||||
Config[TMVCConfigKey.ViewPath] := dotEnv.Env('dmvc.view_path', 'templates');
|
||||
//use cache for server side views (use "false" in debug and "true" in production for faster performances
|
||||
Config[TMVCConfigKey.ViewCache] := dotEnv.Env('dmvc.view_cache', 'false');
|
||||
//Max Record Count for automatic Entities CRUD
|
||||
Config[TMVCConfigKey.MaxEntitiesRecordCount] := dotEnv.Env('dmvc.max_entities_record_count', IntToStr(TMVCConstants.MAX_RECORD_COUNT));
|
||||
//Enable Server Signature in response
|
||||
Config[TMVCConfigKey.ExposeServerSignature] := dotEnv.Env('dmvc.expose_server_signature', 'false');
|
||||
//Enable X-Powered-By Header in response
|
||||
Config[TMVCConfigKey.ExposeXPoweredBy] := dotEnv.Env('dmvc.expose_x_powered_by', 'true');
|
||||
// Max request size in bytes
|
||||
Config[TMVCConfigKey.MaxRequestSize] := dotEnv.Env('dmvc.max_request_size', IntToStr(TMVCConstants.DEFAULT_MAX_REQUEST_SIZE));
|
||||
end);
|
||||
FMVC.AddController(TMyController);
|
||||
|
||||
|
||||
// Analytics middleware generates a csv log, useful to do traffic analysis
|
||||
//FMVC.AddMiddleware(TMVCAnalyticsMiddleware.Create(GetAnalyticsDefaultLogger));
|
||||
|
||||
// The folder mapped as documentroot for TMVCStaticFilesMiddleware must exists!
|
||||
//FMVC.AddMiddleware(TMVCStaticFilesMiddleware.Create('/static', TPath.Combine(ExtractFilePath(GetModuleName(HInstance)), 'www')));
|
||||
|
||||
// Trace middlewares produces a much detailed log for debug purposes
|
||||
//FMVC.AddMiddleware(TMVCTraceMiddleware.Create);
|
||||
|
||||
// CORS middleware handles... well, CORS
|
||||
//FMVC.AddMiddleware(TMVCCORSMiddleware.Create);
|
||||
|
||||
// Simplifies TMVCActiveRecord connection definition
|
||||
|
||||
FMVC.AddMiddleware(TMVCActiveRecordMiddleware.Create(
|
||||
dotEnv.Env('firedac.connection_definition_name', 'activerecorddb'),
|
||||
dotEnv.Env('firedac.connection_definitions_filename', '')
|
||||
));
|
||||
|
||||
|
||||
|
||||
// Compression middleware must be the last in the chain, just before the ETag, if present.
|
||||
//FMVC.AddMiddleware(TMVCCompressionMiddleware.Create);
|
||||
|
||||
// ETag middleware must be the latest in the chain
|
||||
//FMVC.AddMiddleware(TMVCETagMiddleware.Create);
|
||||
|
||||
|
||||
|
||||
{
|
||||
FMVC.OnWebContextCreate(
|
||||
procedure(const Context: TWebContext)
|
||||
begin
|
||||
// Initialize services to make them accessibile from Context
|
||||
// Context.CustomIntfObject := TMyService.Create;
|
||||
end);
|
||||
|
||||
FMVC.OnWebContextDestroy(
|
||||
procedure(const Context: TWebContext)
|
||||
begin
|
||||
//Cleanup services, if needed
|
||||
end);
|
||||
}
|
||||
end;
|
||||
|
||||
procedure TMyWebModule.WebModuleDestroy(Sender: TObject);
|
||||
begin
|
||||
FMVC.Free;
|
||||
end;
|
||||
|
||||
end.
|
97
samples/services_injection/services_injection.dpr
Normal file
97
samples/services_injection/services_injection.dpr
Normal file
@ -0,0 +1,97 @@
|
||||
program services_injection;
|
||||
|
||||
{$APPTYPE CONSOLE}
|
||||
|
||||
uses
|
||||
System.SysUtils,
|
||||
MVCFramework,
|
||||
MVCFramework.Logger,
|
||||
MVCFramework.DotEnv,
|
||||
MVCFramework.Commons,
|
||||
MVCFramework.Signal,
|
||||
Web.ReqMulti,
|
||||
Web.WebReq,
|
||||
Web.WebBroker,
|
||||
IdContext,
|
||||
IdHTTPWebBrokerBridge,
|
||||
MainControllerU in 'MainControllerU.pas',
|
||||
WebModuleU in 'WebModuleU.pas' {MyWebModule: TWebModule},
|
||||
Services.PeopleU in 'Services.PeopleU.pas',
|
||||
Services.InterfacesU in 'Services.InterfacesU.pas',
|
||||
Entities in '..\commons\Entities.pas',
|
||||
MVCFramework.Router in '..\..\sources\MVCFramework.Router.pas',
|
||||
MVCFramework.Container in '..\..\sources\MVCFramework.Container.pas',
|
||||
Services.ConnectionU in 'Services.ConnectionU.pas',
|
||||
Services.RegistrationU in 'Services.RegistrationU.pas';
|
||||
|
||||
{$R *.res}
|
||||
|
||||
|
||||
procedure RunServer(APort: Integer);
|
||||
var
|
||||
LServer: TIdHTTPWebBrokerBridge;
|
||||
begin
|
||||
|
||||
LServer := TIdHTTPWebBrokerBridge.Create(nil);
|
||||
try
|
||||
LServer.OnParseAuthentication := TMVCParseAuthentication.OnParseAuthentication;
|
||||
LServer.DefaultPort := APort;
|
||||
LServer.KeepAlive := True;
|
||||
LServer.MaxConnections := dotEnv.Env('dmvc.webbroker.max_connections', 0);
|
||||
LServer.ListenQueue := dotEnv.Env('dmvc.indy.listen_queue', 500);
|
||||
|
||||
LServer.Active := True;
|
||||
LogI('Listening on port ' + APort.ToString);
|
||||
LogI('Application started. Press Ctrl+C to shut down.');
|
||||
WaitForTerminationSignal;
|
||||
EnterInShutdownState;
|
||||
LServer.Active := False;
|
||||
finally
|
||||
LServer.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
begin
|
||||
{ Enable ReportMemoryLeaksOnShutdown during debug }
|
||||
ReportMemoryLeaksOnShutdown := True;
|
||||
IsMultiThread := True;
|
||||
|
||||
// DMVCFramework Specific Configuration
|
||||
// When MVCSerializeNulls = True empty nullables and nil are serialized as json null.
|
||||
// When MVCSerializeNulls = False empty nullables and nil are not serialized at all.
|
||||
MVCSerializeNulls := True;
|
||||
UseConsoleLogger := True;
|
||||
|
||||
LogI('** DMVCFramework Server ** build ' + DMVCFRAMEWORK_VERSION);
|
||||
try
|
||||
dotEnvConfigure(
|
||||
function: IMVCDotEnv
|
||||
begin
|
||||
Result := NewDotEnv
|
||||
.UseStrategy(TMVCDotEnvPriority.FileThenEnv)
|
||||
.UseLogger(procedure(LogItem: String)
|
||||
begin
|
||||
LogD('dotEnv: ' + LogItem);
|
||||
end)
|
||||
.Build();
|
||||
end);
|
||||
|
||||
RegisterServices(DefaultMVCServiceContainer);
|
||||
DefaultMVCServiceContainer.Build;
|
||||
|
||||
if dotEnv.Env('dmvc.profiler.enabled', false) then
|
||||
begin
|
||||
Profiler.ProfileLogger := Log;
|
||||
Profiler.WarningThreshold := dotEnv.Env('dmvc.profiler.warning_threshold', 2000);
|
||||
end;
|
||||
|
||||
if WebRequestHandler <> nil then
|
||||
WebRequestHandler.WebModuleClass := WebModuleClass;
|
||||
WebRequestHandlerProc.MaxConnections := dotEnv.Env('dmvc.handler.max_connections', 1024);
|
||||
|
||||
RunServer(dotEnv.Env('dmvc.server.port', 8080));
|
||||
except
|
||||
on E: Exception do
|
||||
LogF(E.ClassName + ': ' + E.Message);
|
||||
end;
|
||||
end.
|
1034
samples/services_injection/services_injection.dproj
Normal file
1034
samples/services_injection/services_injection.dproj
Normal file
File diff suppressed because it is too large
Load Diff
@ -26,6 +26,7 @@ unit MVCFramework.ActiveRecord;
|
||||
|
||||
{$I dmvcframework.inc}
|
||||
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
@ -61,10 +62,23 @@ type
|
||||
EMVCActiveRecordVersionedItemNotFound = class(EMVCActiveRecordNotFound)
|
||||
end;
|
||||
|
||||
EMVCActiveRecordTransactionContext = class(EMVCActiveRecord)
|
||||
end;
|
||||
|
||||
|
||||
TMVCActiveRecordClass = class of TMVCActiveRecord;
|
||||
TMVCActiveRecord = class;
|
||||
|
||||
{$IF Defined(CUSTOM_MANAGED_RECORDS)}
|
||||
TMVCTransactionContext = record
|
||||
private
|
||||
fConnection: TFDConnection;
|
||||
public
|
||||
class operator Finalize(var Dest: TMVCTransactionContext);
|
||||
class operator Assign (var Dest: TMVCTransactionContext; const [ref] Src: TMVCTransactionContext);
|
||||
constructor Create(Dummy: Integer); overload;
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
TMVCActiveRecordFieldOption = (
|
||||
/// <summary>
|
||||
@ -588,6 +602,9 @@ type
|
||||
class function All<T: TMVCActiveRecord, constructor>: TObjectList<T>; overload;
|
||||
class function DeleteRQL<T: TMVCActiveRecord>(const RQL: string = ''): Int64; overload;
|
||||
class function Count<T: TMVCActiveRecord>(const RQL: string = ''): Int64; overload;
|
||||
{$IF Defined(CUSTOM_MANAGED_RECORDS)}
|
||||
class function UseTransactionContext: TMVCTransactionContext;
|
||||
{$ENDIF}
|
||||
|
||||
{ Where }
|
||||
class function Where<T: TMVCActiveRecord, constructor>(const SQLWhere: string;
|
||||
@ -1780,7 +1797,8 @@ begin
|
||||
if not lFound then
|
||||
begin
|
||||
if RaiseExceptionIfNotFound then
|
||||
raise EMVCActiveRecordNotFound.Create('No data found')
|
||||
raise EMVCActiveRecordNotFound.CreateFmt('No data found for key [Entity: %s][PK: %s]',
|
||||
[aActiveRecord.ClassName, aActiveRecord.fTableMap.fPrimaryKeyFieldName])
|
||||
else
|
||||
FreeAndNil(Result);
|
||||
end;
|
||||
@ -2216,6 +2234,13 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
{$IF Defined(CUSTOM_MANAGED_RECORDS)}
|
||||
class function TMVCActiveRecordHelper.UseTransactionContext: TMVCTransactionContext;
|
||||
begin
|
||||
Result := TMVCTransactionContext.Create(0);
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
class function TMVCActiveRecordHelper.TryGetRQLQuery<T>(
|
||||
const QueryName: String; out NamedRQLQuery: TRQLQueryWithName): Boolean;
|
||||
var
|
||||
@ -4759,6 +4784,43 @@ begin
|
||||
Create(True);
|
||||
end;
|
||||
|
||||
{ TMVCTransactionContext }
|
||||
|
||||
{$IF Defined(CUSTOM_MANAGED_RECORDS)}
|
||||
|
||||
constructor TMVCTransactionContext.Create(Dummy: Integer);
|
||||
begin
|
||||
fConnection := nil;
|
||||
end;
|
||||
|
||||
class operator TMVCTransactionContext.Assign(var Dest: TMVCTransactionContext; const [ref] Src: TMVCTransactionContext);
|
||||
begin
|
||||
if Assigned(Src.fConnection) then
|
||||
begin
|
||||
Dest.fConnection := nil;
|
||||
raise EMVCActiveRecordTransactionContext.Create('Transaction Context cannot be copied nor passed by value');
|
||||
end;
|
||||
Dest.fConnection := TMVCActiveRecord.CurrentConnection;
|
||||
Dest.fConnection.StartTransaction;
|
||||
end;
|
||||
|
||||
class operator TMVCTransactionContext.Finalize(var Dest: TMVCTransactionContext);
|
||||
begin
|
||||
if Dest.fConnection <> nil then
|
||||
begin
|
||||
if ExceptAddr <> nil then
|
||||
begin
|
||||
Dest.fConnection.Rollback;
|
||||
end
|
||||
else
|
||||
begin
|
||||
Dest.fConnection.Commit;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
{$ENDIF}
|
||||
|
||||
initialization
|
||||
|
||||
gConnectionsLock := TObject.Create;
|
||||
|
@ -43,7 +43,7 @@ uses
|
||||
System.Generics.Collections,
|
||||
MVCFramework.DuckTyping,
|
||||
JsonDataObjects,
|
||||
MVCFramework.DotEnv;
|
||||
MVCFramework.DotEnv, MVCFramework.Container;
|
||||
|
||||
{$I dmvcframeworkbuildconsts.inc}
|
||||
|
||||
@ -564,6 +564,7 @@ type
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
procedure Freeze;
|
||||
function Frozen: Boolean;
|
||||
function Keys: TArray<string>;
|
||||
function ToString: string; override;
|
||||
procedure SaveToFile(const AFileName: string);
|
||||
@ -766,7 +767,6 @@ type
|
||||
function dotEnv: IMVCDotEnv; overload;
|
||||
procedure dotEnvConfigure(const dotEnvDelegate: TFunc<IMVCDotEnv>);
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
@ -780,11 +780,11 @@ uses
|
||||
|
||||
var
|
||||
GlobalAppName, GlobalAppPath, GlobalAppExe: string;
|
||||
|
||||
var
|
||||
GdotEnv: IMVCDotEnv = nil;
|
||||
GdotEnvDelegate: TFunc<IMVCDotEnv> = nil;
|
||||
|
||||
|
||||
function URLEncode(const Value: string): string; overload;
|
||||
begin
|
||||
{$IF defined(BERLINORBETTER)}
|
||||
@ -1060,6 +1060,11 @@ begin
|
||||
FFreezed := True;
|
||||
end;
|
||||
|
||||
function TMVCConfig.Frozen: Boolean;
|
||||
begin
|
||||
Result := FFreezed;
|
||||
end;
|
||||
|
||||
function TMVCConfig.GetValue(const AIndex: string): string;
|
||||
begin
|
||||
if FConfig.ContainsKey(AIndex) then
|
||||
@ -1761,6 +1766,7 @@ begin
|
||||
Result := ReasonStringByHTTPStatusCode(HTTPStatusCode);
|
||||
end;
|
||||
|
||||
|
||||
procedure dotEnvConfigure(const dotEnvDelegate: TFunc<IMVCDotEnv>);
|
||||
begin
|
||||
if GdotEnv <> nil then
|
||||
|
366
sources/MVCFramework.Container.pas
Normal file
366
sources/MVCFramework.Container.pas
Normal file
@ -0,0 +1,366 @@
|
||||
unit MVCFramework.Container;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.Generics.Collections, System.Rtti, System.SysUtils, System.TypInfo;
|
||||
|
||||
|
||||
type
|
||||
{$SCOPEDENUMS ON}
|
||||
TRegistrationType = (Transient, Singleton, SingletonPerRequest);
|
||||
|
||||
TClassOfInterfacedObject = class of TInterfacedObject;
|
||||
|
||||
IMVCServiceContainerResolver = interface
|
||||
['{2C920EC2-001F-40BE-9911-43A65077CADD}']
|
||||
function Resolve(const aTypeInfo: PTypeInfo; const aName: string = ''): IInterface; overload;
|
||||
end;
|
||||
|
||||
IMVCServiceContainer = interface
|
||||
['{1BB3F4A8-DDA1-4526-981C-A0BF877CFFD5}']
|
||||
function RegisterType(const aImplementation: TClassOfInterfacedObject; const aInterface: TGUID; const aName : string = ''; const aRegType: TRegistrationType = TRegistrationType.Transient): IMVCServiceContainer; overload;
|
||||
procedure Build();
|
||||
end;
|
||||
|
||||
EMVCContainerError = class(Exception) end;
|
||||
EMVCContainerErrorUnknownService = class(EMVCContainerError) end;
|
||||
EMVCContainerErrorInterfaceNotSupported = class(EMVCContainerError) end;
|
||||
EMVCContainerErrorUnknownConstructor = class(EMVCContainerError) end;
|
||||
|
||||
|
||||
function DefaultMVCServiceContainer: IMVCServiceContainer;
|
||||
function NewMVCServiceContainer: IMVCServiceContainer;
|
||||
function NewServiceContainerResolver: IMVCServiceContainerResolver; overload;
|
||||
function NewServiceContainerResolver(Container: IMVCServiceContainer): IMVCServiceContainerResolver; overload;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
MVCFramework.Rtti.Utils, MVCFramework;
|
||||
|
||||
type
|
||||
IMVCServiceInternalResolver = interface
|
||||
['{81527509-BA94-48C1-A030-E26F1FC9BFF5}']
|
||||
function Resolve(const ServiceContainerResolver: IMVCServiceContainerResolver; const aTypeInfo: PTypeInfo; const aName: string = ''): IInterface;
|
||||
function ResolveEx(const ServiceContainerResolver: IMVCServiceContainerResolver; const aTypeInfo: PTypeInfo; const aName: string; out ServiceKey: String; out RegType: TRegistrationType): IInterface; overload;
|
||||
end;
|
||||
|
||||
TRegistration = class
|
||||
public
|
||||
Intf: TGUID;
|
||||
Clazz: TClassOfInterfacedObject;
|
||||
RttiType: TRttiType;
|
||||
Instance: IInterface;
|
||||
RegistrationType: TRegistrationType;
|
||||
end;
|
||||
|
||||
TMVCServiceContainer = class(TInterfacedObject, IMVCServiceContainer, IMVCServiceInternalResolver)
|
||||
private
|
||||
fBuilt: Boolean;
|
||||
fRegistry: TObjectDictionary<string, TRegistration>;
|
||||
function CreateServiceWithDependencies(
|
||||
const ServiceContainerResolver: IMVCServiceContainerResolver;
|
||||
const ServiceClass: TClassOfInterfacedObject;
|
||||
const ConstructorMethod: TRttiMethod): TInterfacedObject;
|
||||
protected
|
||||
class function GetKey(const aGUID: TGUID; const aName: String): String;
|
||||
constructor Create; virtual;
|
||||
destructor Destroy; override;
|
||||
public
|
||||
function RegisterType(const aImplementation: TClassOfInterfacedObject; const aInterface: TGUID; const aName : string = ''; const aRegType: TRegistrationType = TRegistrationType.Transient): IMVCServiceContainer; overload;
|
||||
function Resolve(const ServiceContainerResolver: IMVCServiceContainerResolver; const aTypeInfo: PTypeInfo; const aName: string = ''): IInterface; overload;
|
||||
function ResolveEx(const ServiceContainerResolver: IMVCServiceContainerResolver; const aTypeInfo: PTypeInfo; const aName: string; out ServiceKey: String; out RegType: TRegistrationType): IInterface; overload;
|
||||
procedure Build();
|
||||
end;
|
||||
|
||||
TMVCServiceContainerAdapter = class(TInterfacedObject, IMVCServiceContainerResolver)
|
||||
private
|
||||
fCachedServices: TDictionary<String, IInterface>;
|
||||
fContainer: IMVCServiceInternalResolver;
|
||||
protected
|
||||
function Resolve(const aTypeInfo: PTypeInfo; const aName: string = ''): IInterface; overload;
|
||||
public
|
||||
constructor Create(Container: IMVCServiceContainer);
|
||||
destructor Destroy; override;
|
||||
end;
|
||||
|
||||
|
||||
var
|
||||
gDefaultMVCServiceContainer: IMVCServiceContainer = nil;
|
||||
gLock: TObject = nil;
|
||||
|
||||
{ TMVCServiceContainer }
|
||||
|
||||
function TMVCServiceContainer.CreateServiceWithDependencies(
|
||||
const ServiceContainerResolver: IMVCServiceContainerResolver;
|
||||
const ServiceClass: TClassOfInterfacedObject;
|
||||
const ConstructorMethod: TRttiMethod): TInterfacedObject;
|
||||
var
|
||||
lActionFormalParams: TArray<TRttiParameter>;
|
||||
lActualParams: TArray<TValue>;
|
||||
I: Integer;
|
||||
lIntf, lOutIntf: IInterface;
|
||||
begin
|
||||
if ConstructorMethod <> nil then
|
||||
begin
|
||||
lActionFormalParams := ConstructorMethod.GetParameters;
|
||||
SetLength(lActualParams, Length(lActionFormalParams));
|
||||
if Length(lActionFormalParams) > 0 then
|
||||
begin
|
||||
for I := 0 to Length(lActionFormalParams) - 1 do
|
||||
begin
|
||||
if ServiceContainerResolver = nil then
|
||||
lIntf := Resolve(nil, lActionFormalParams[I].ParamType.Handle)
|
||||
else
|
||||
lIntf := ServiceContainerResolver.Resolve(lActionFormalParams[I].ParamType.Handle);
|
||||
if not Supports(lIntf, lActionFormalParams[I].ParamType.Handle.TypeData.GUID, lOutIntf) then
|
||||
begin
|
||||
raise EMVCContainerError.CreateFmt('Cannot inject parameter %s: %s into constructor of %s', [
|
||||
lActionFormalParams[I].name,
|
||||
lActionFormalParams[I].ParamType.ToString,
|
||||
ServiceClass.ClassName
|
||||
]);
|
||||
end;
|
||||
TValue.Make(@lOutIntf, lActionFormalParams[I].ParamType.Handle, lActualParams[I]);
|
||||
end;
|
||||
end;
|
||||
Result := TInterfacedObject(ConstructorMethod.Invoke(ServiceClass, lActualParams).AsObject);
|
||||
end
|
||||
else
|
||||
begin
|
||||
Result := TInterfacedObject(TRttiUtils.CreateObject(ServiceClass.QualifiedClassName));
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
constructor TMVCServiceContainer.Create;
|
||||
begin
|
||||
inherited;
|
||||
fRegistry := TObjectDictionary<String, TRegistration>.Create([doOwnsValues]);
|
||||
fBuilt := False;
|
||||
end;
|
||||
|
||||
destructor TMVCServiceContainer.Destroy;
|
||||
begin
|
||||
fRegistry.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
class function TMVCServiceContainer.GetKey(const aGUID: TGUID; const aName: String): String;
|
||||
begin
|
||||
Result := aGUID.ToString + '_' + aName;
|
||||
end;
|
||||
|
||||
function TMVCServiceContainer.RegisterType(const aImplementation: TClassOfInterfacedObject; const aInterface: TGUID;
|
||||
const aName: string; const aRegType: TRegistrationType): IMVCServiceContainer;
|
||||
var
|
||||
lReg: TRegistration;
|
||||
begin
|
||||
if fBuilt then
|
||||
begin
|
||||
raise EMVCContainerError.Create('Cannot register new service if the container has been already built');
|
||||
end;
|
||||
|
||||
if Supports(aImplementation, aInterface) then
|
||||
begin
|
||||
lReg := TRegistration.Create;
|
||||
lReg.Clazz := aImplementation;
|
||||
lReg.RttiType := TRttiUtils.GlContext.GetType(lReg.Clazz);
|
||||
lReg.RegistrationType := aRegType;
|
||||
if not fRegistry.TryAdd(GetKey(aInterface, aName), lReg) then
|
||||
begin
|
||||
raise EMVCContainerError.CreateFmt('Cannot register duplicated service "%s"',[GetKey(aInterface, aName)]);
|
||||
end;
|
||||
end
|
||||
else
|
||||
begin
|
||||
raise EMVCContainerErrorUnknownService.CreateFmt('"%s" doesn''t supports requested interface', [aImplementation.QualifiedClassName]);
|
||||
end;
|
||||
Result := Self;
|
||||
end;
|
||||
|
||||
function TMVCServiceContainer.Resolve(const ServiceContainerResolver: IMVCServiceContainerResolver; const aTypeInfo: PTypeInfo; const aName: string): IInterface;
|
||||
var
|
||||
lReg: TRegistration;
|
||||
lTypeInfo: PTypeInfo;
|
||||
lType: TRttiType;
|
||||
lService: TObject;
|
||||
begin
|
||||
if not fBuilt then
|
||||
begin
|
||||
raise EMVCContainerError.Create('Container has not been built');
|
||||
end;
|
||||
lTypeInfo := aTypeInfo;
|
||||
if not fRegistry.TryGetValue(GetKey(lTypeInfo.TypeData.GUID, aName), lReg) then
|
||||
begin
|
||||
raise EMVCContainerErrorUnknownService.CreateFmt('Unknown service "%s" with name "%s"', [lTypeInfo.Name, aName])
|
||||
end;
|
||||
lType := lReg.RttiType;
|
||||
|
||||
case lReg.RegistrationType of
|
||||
TRegistrationType.Transient, TRegistrationType.SingletonPerRequest:
|
||||
begin
|
||||
lService := CreateServiceWithDependencies(ServiceContainerResolver, lReg.Clazz, TRttiUtils.GetFirstDeclaredConstructor(lType));
|
||||
Supports(lService, lTypeInfo.TypeData.GUID, Result);
|
||||
end;
|
||||
|
||||
TRegistrationType.Singleton:
|
||||
begin
|
||||
if lReg.Instance = nil then
|
||||
begin
|
||||
TMonitor.Enter(Self);
|
||||
try
|
||||
if lReg.Instance = nil then
|
||||
begin
|
||||
lService := CreateServiceWithDependencies(ServiceContainerResolver, lReg.Clazz, TRttiUtils.GetFirstDeclaredConstructor(lType));
|
||||
Supports(lService, lTypeInfo.TypeData.GUID, lReg.Instance)
|
||||
end;
|
||||
finally
|
||||
TMonitor.Exit(Self)
|
||||
end;
|
||||
end;
|
||||
Supports(lReg.Instance, lTypeInfo.TypeData.GUID, Result);
|
||||
end;
|
||||
else
|
||||
raise EMVCContainerError.Create('Unsupported RegistrationType');
|
||||
end;
|
||||
end;
|
||||
|
||||
function TMVCServiceContainer.ResolveEx(const ServiceContainerResolver: IMVCServiceContainerResolver; const aTypeInfo: PTypeInfo; const aName: string;
|
||||
out ServiceKey: String; out RegType: TRegistrationType): IInterface;
|
||||
var
|
||||
lReg: TRegistration;
|
||||
lTypeInfo: PTypeInfo;
|
||||
lType: TRttiType;
|
||||
lService: TObject;
|
||||
lServiceKey: string;
|
||||
begin
|
||||
if not fBuilt then
|
||||
begin
|
||||
raise EMVCContainerError.Create('Container has not been built');
|
||||
end;
|
||||
lTypeInfo := aTypeInfo;
|
||||
lServiceKey := GetKey(lTypeInfo.TypeData.GUID, aName);
|
||||
if not fRegistry.TryGetValue(lServiceKey, lReg) then
|
||||
begin
|
||||
raise EMVCContainerErrorUnknownService.CreateFmt('Unknown service "%s" with name "%s"', [lTypeInfo.Name, aName])
|
||||
end;
|
||||
lType := lReg.RttiType;
|
||||
|
||||
RegType := lReg.RegistrationType;
|
||||
ServiceKey := lServiceKey;
|
||||
case lReg.RegistrationType of
|
||||
TRegistrationType.Transient, TRegistrationType.SingletonPerRequest:
|
||||
begin
|
||||
lService := CreateServiceWithDependencies(ServiceContainerResolver, lReg.Clazz, TRttiUtils.GetFirstDeclaredConstructor(lType));
|
||||
Supports(lService, lTypeInfo.TypeData.GUID, Result);
|
||||
{rtSingletonPerRequest is destroyed by the adapter owned by Context}
|
||||
end;
|
||||
|
||||
TRegistrationType.Singleton:
|
||||
begin
|
||||
if lReg.Instance = nil then
|
||||
begin
|
||||
TMonitor.Enter(Self);
|
||||
try
|
||||
if lReg.Instance = nil then
|
||||
begin
|
||||
lService := CreateServiceWithDependencies(ServiceContainerResolver, lReg.Clazz, TRttiUtils.GetFirstDeclaredConstructor(lType));
|
||||
Supports(lService, lTypeInfo.TypeData.GUID, lReg.Instance)
|
||||
end;
|
||||
finally
|
||||
TMonitor.Exit(Self)
|
||||
end;
|
||||
end;
|
||||
Supports(lReg.Instance, lTypeInfo.TypeData.GUID, Result);
|
||||
end;
|
||||
else
|
||||
raise EMVCContainerError.Create('Unsupported RegistrationType');
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMVCServiceContainer.Build;
|
||||
begin
|
||||
fBuilt := True;
|
||||
end;
|
||||
|
||||
function DefaultMVCServiceContainer: IMVCServiceContainer;
|
||||
begin
|
||||
if gDefaultMVCServiceContainer = nil then
|
||||
begin
|
||||
TMonitor.Enter(gLock);
|
||||
try
|
||||
if gDefaultMVCServiceContainer = nil then
|
||||
begin
|
||||
gDefaultMVCServiceContainer := TMVCServiceContainer.Create;
|
||||
end;
|
||||
finally
|
||||
TMonitor.Exit(gLock);
|
||||
end;
|
||||
end;
|
||||
Result := gDefaultMVCServiceContainer;
|
||||
end;
|
||||
|
||||
function NewMVCServiceContainer: IMVCServiceContainer;
|
||||
begin
|
||||
Result := TMVCServiceContainer.Create;
|
||||
end;
|
||||
|
||||
{ TMVCServiceContainerAdapter }
|
||||
|
||||
constructor TMVCServiceContainerAdapter.Create(Container: IMVCServiceContainer);
|
||||
begin
|
||||
inherited Create;
|
||||
fCachedServices := TDictionary<String, IInterface>.Create;
|
||||
fContainer := Container as IMVCServiceInternalResolver;
|
||||
end;
|
||||
|
||||
destructor TMVCServiceContainerAdapter.Destroy;
|
||||
begin
|
||||
fCachedServices.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
function TMVCServiceContainerAdapter.Resolve(const aTypeInfo: PTypeInfo; const aName: string): IInterface;
|
||||
var
|
||||
lKey: string;
|
||||
lIntf: IInterface;
|
||||
lRegType: TRegistrationType;
|
||||
begin
|
||||
lKey := TMVCServiceContainer.GetKey(aTypeInfo.TypeData.GUID, aName);
|
||||
if fCachedServices.TryGetValue(lKey, lIntf) then
|
||||
begin
|
||||
Supports(lIntf, aTypeInfo.TypeData.GUID, Result);
|
||||
end
|
||||
else
|
||||
begin
|
||||
Result := fContainer.ResolveEx(Self, aTypeInfo, aName, lKey, lRegType);
|
||||
if lRegType = TRegistrationType.SingletonPerRequest then
|
||||
begin
|
||||
fCachedServices.Add(lKey, Result);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
function NewServiceContainerResolver: IMVCServiceContainerResolver;
|
||||
begin
|
||||
Result := TMVCServiceContainerAdapter.Create(DefaultMVCServiceContainer);
|
||||
end;
|
||||
|
||||
function NewServiceContainerResolver(Container: IMVCServiceContainer) : IMVCServiceContainerResolver;
|
||||
begin
|
||||
Result := TMVCServiceContainerAdapter.Create(Container);
|
||||
end;
|
||||
|
||||
|
||||
initialization
|
||||
|
||||
gLock := TObject.Create;
|
||||
|
||||
finalization
|
||||
|
||||
gLock.Free;
|
||||
|
||||
end.
|
@ -315,7 +315,7 @@ begin
|
||||
end;
|
||||
if lMsg.Trim.IsEmpty then
|
||||
lMsg := '<EOF>';
|
||||
raise ERQLException.CreateFmt('[Error] %s (column %d - found %s)', [message, fCurIdx, lMsg]);
|
||||
raise ERQLException.CreateFmt('[Error] %s (column %d - found %s)', [message, fCurIdx, lMsg]) at AddressOfReturnAddress;
|
||||
end;
|
||||
|
||||
procedure TRQL2SQL.Execute(
|
||||
|
@ -58,6 +58,7 @@ type
|
||||
FMethodToCall: TRttiMethod;
|
||||
FControllerClazz: TMVCControllerClazz;
|
||||
FControllerCreateAction: TMVCControllerCreateAction;
|
||||
FControllerInjectableConstructor: TRttiMethod;
|
||||
FActionParamsCache: TMVCStringObjectDictionary<TMVCActionParamCacheItem>;
|
||||
function GetAttribute<T: TCustomAttribute>(const AAttributes: TArray<TCustomAttribute>): T;
|
||||
function GetFirstMediaType(const AContentType: string): string;
|
||||
@ -106,13 +107,14 @@ type
|
||||
property MethodToCall: TRttiMethod read FMethodToCall;
|
||||
property ControllerClazz: TMVCControllerClazz read FControllerClazz;
|
||||
property ControllerCreateAction: TMVCControllerCreateAction read FControllerCreateAction;
|
||||
property ControllerInjectableConstructor: TRttiMethod read FControllerInjectableConstructor;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.TypInfo,
|
||||
System.NetEncoding;
|
||||
System.NetEncoding, MVCFramework.Rtti.Utils, MVCFramework.Container;
|
||||
|
||||
{ TMVCRouter }
|
||||
|
||||
@ -160,6 +162,8 @@ var
|
||||
LProduceAttribute: MVCProducesAttribute;
|
||||
lURLSegment: string;
|
||||
LItem: String;
|
||||
lConstructors: TArray<TRttiMethod>;
|
||||
lConstructor: TRttiMethod;
|
||||
// JUST FOR DEBUG
|
||||
// lMethodCompatible: Boolean;
|
||||
// lContentTypeCompatible: Boolean;
|
||||
@ -268,6 +272,15 @@ begin
|
||||
FMethodToCall := LMethod;
|
||||
FControllerClazz := LControllerDelegate.Clazz;
|
||||
FControllerCreateAction := LControllerDelegate.CreateAction;
|
||||
FControllerInjectableConstructor := nil;
|
||||
|
||||
// select the constructor with the most mumber of parameters
|
||||
if not Assigned(FControllerCreateAction) then
|
||||
begin
|
||||
FControllerInjectableConstructor := TRttiUtils.GetConstructorWithAttribute<MVCInjectAttribute>(LRttiType);
|
||||
end;
|
||||
// end - select the constructor with the most mumber of parameters
|
||||
|
||||
LProduceAttribute := GetAttribute<MVCProducesAttribute>(LAttributes);
|
||||
if LProduceAttribute <> nil then
|
||||
begin
|
||||
|
@ -34,7 +34,7 @@ uses
|
||||
System.Rtti,
|
||||
System.Generics.Collections,
|
||||
System.SysUtils,
|
||||
Data.DB;
|
||||
Data.DB, MVCFramework.Logger;
|
||||
|
||||
type
|
||||
|
||||
@ -90,6 +90,8 @@ type
|
||||
class function FindType(AQualifiedName: string): TRttiType;
|
||||
class function GetGUID<T>: TGUID;
|
||||
class function GetArrayContainedRTTIType(const RTTIType: TRttiType): TRttiType;
|
||||
class function GetConstructorWithAttribute<T:TCustomAttribute>(const RTTIType: TRttiType): TRttiMethod;
|
||||
class function GetFirstDeclaredConstructor(const RTTIType: TRttiType): TRttiMethod;
|
||||
end;
|
||||
|
||||
{$IF not defined(BERLINORBETTER)}
|
||||
@ -105,7 +107,7 @@ implementation
|
||||
|
||||
uses
|
||||
MVCFramework.DuckTyping,
|
||||
MVCFramework.Serializer.Commons;
|
||||
MVCFramework.Serializer.Commons, MVCFramework.Commons;
|
||||
|
||||
class function TRttiUtils.MethodCall(AObject: TObject; AMethodName: string; AParameters: array of TValue;
|
||||
ARaiseExceptionIfNotFound: Boolean): TValue;
|
||||
@ -180,6 +182,40 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
class function TRttiUtils.GetConstructorWithAttribute<T>(const RTTIType: TRttiType): TRttiMethod;
|
||||
var
|
||||
lConstructors: TArray<TRttiMethod>;
|
||||
lConstructor: TRttiMethod;
|
||||
begin
|
||||
Result := nil;
|
||||
lConstructors := RttiType.GetMethods('Create');
|
||||
for lConstructor in lConstructors do
|
||||
begin
|
||||
if lConstructor.HasAttribute<T> then
|
||||
begin
|
||||
Result := lConstructor;
|
||||
break; { the first wins }
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
class function TRttiUtils.GetFirstDeclaredConstructor(const RTTIType: TRttiType): TRttiMethod;
|
||||
var
|
||||
lConstructors: TArray<TRttiMethod>;
|
||||
lConstructor: TRttiMethod;
|
||||
begin
|
||||
Result := nil;
|
||||
lConstructors := RttiType.GetDeclaredMethods;
|
||||
for lConstructor in lConstructors do
|
||||
begin
|
||||
if lConstructor.IsConstructor and (lConstructor.Visibility = TMembervisibility.mvPublic) then
|
||||
begin
|
||||
Result := lConstructor;
|
||||
Break;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
class function TRttiUtils.GetField(AObject: TObject; const APropertyName: string): TValue;
|
||||
var
|
||||
Field: TRttiField;
|
||||
|
@ -54,6 +54,7 @@ uses
|
||||
MVCFramework.Session,
|
||||
MVCFramework.DuckTyping,
|
||||
MVCFramework.Logger,
|
||||
MVCFramework.Container,
|
||||
MVCFramework.ApplicationSession,
|
||||
MVCFramework.Serializer.Intf,
|
||||
|
||||
@ -337,6 +338,14 @@ type
|
||||
|
||||
end;
|
||||
|
||||
MVCInjectAttribute = class(TCustomAttribute)
|
||||
private
|
||||
fServiceName: String;
|
||||
public
|
||||
constructor Create(ServiceName: String = '');
|
||||
property ServiceName: String read fServiceName;
|
||||
end;
|
||||
|
||||
// test
|
||||
// TMVCHackHTTPAppRequest = class(TIdHTTPAppRequest)
|
||||
// private
|
||||
@ -522,16 +531,17 @@ type
|
||||
|
||||
TWebContext = class
|
||||
private
|
||||
FRequest: TMVCWebRequest;
|
||||
FResponse: TMVCWebResponse;
|
||||
FConfig: TMVCConfig;
|
||||
FSerializers: TDictionary<string, IMVCSerializer>;
|
||||
FIsSessionStarted: Boolean;
|
||||
FSessionMustBeClose: Boolean;
|
||||
FLoggedUser: TUser;
|
||||
FWebSession: TMVCWebSession;
|
||||
FData: TMVCStringDictionary;
|
||||
fRequest: TMVCWebRequest;
|
||||
fResponse: TMVCWebResponse;
|
||||
fConfig: TMVCConfig;
|
||||
fSerializers: TDictionary<string, IMVCSerializer>;
|
||||
fIsSessionStarted: Boolean;
|
||||
fSessionMustBeClose: Boolean;
|
||||
fLoggedUser: TUser;
|
||||
fWebSession: TMVCWebSession;
|
||||
fData: TMVCStringDictionary;
|
||||
fIntfObject: IInterface;
|
||||
fServiceContainerResolver: IMVCServiceContainerResolver;
|
||||
function GetWebSession: TMVCWebSession;
|
||||
function GetLoggedUser: TUser;
|
||||
function GetParamsTable: TMVCRequestParamsTable;
|
||||
@ -548,7 +558,7 @@ type
|
||||
const ASessionTimeout: Integer): TMVCWebSession;
|
||||
function GetData: TMVCStringDictionary;
|
||||
public
|
||||
constructor Create(const ARequest: TWebRequest; const AResponse: TWebResponse;
|
||||
constructor Create(const AServiceContainerResolver: IMVCServiceContainerResolver; const ARequest: TWebRequest; const AResponse: TWebResponse;
|
||||
const AConfig: TMVCConfig; const ASerializers: TDictionary<string, IMVCSerializer>);
|
||||
destructor Destroy; override;
|
||||
|
||||
@ -570,6 +580,7 @@ type
|
||||
property CustomIntfObject: IInterface read GetIntfObject write SetIntfObject;
|
||||
property ParamsTable: TMVCRequestParamsTable read GetParamsTable write SetParamsTable;
|
||||
property ActionQualifiedName: String read fActionQualifiedName;
|
||||
property ServiceContainerResolver: IMVCServiceContainerResolver read fServiceContainerResolver;
|
||||
end;
|
||||
|
||||
TMVCJSONRPCExceptionErrorInfo = record
|
||||
@ -664,12 +675,37 @@ type
|
||||
var AIsAuthorized: Boolean);
|
||||
end;
|
||||
|
||||
// std responses
|
||||
|
||||
IMVCResponse = interface
|
||||
['{9DFEC741-EE38-4AC9-9C2C-9EA0D15D08D5}']
|
||||
function GetData: TObject;
|
||||
function GetMessage: string;
|
||||
function GetStatusCode: Integer;
|
||||
function GetHeaders: TStringList;
|
||||
procedure SetData(const Value: TObject);
|
||||
procedure SetMessage(const Value: string);
|
||||
procedure SetHeaders(const Headers: TStringList);
|
||||
procedure SetObjectDictionary(const Value: IMVCObjectDictionary);
|
||||
function GetObjectDictionary: IMVCObjectDictionary;
|
||||
procedure SetStatusCode(const Value: Integer);
|
||||
function GetIgnoredList: TMVCIgnoredList;
|
||||
function HasHeaders: Boolean;
|
||||
function HasBody: Boolean;
|
||||
property StatusCode: Integer read GetStatusCode write SetStatusCode;
|
||||
property Message: string read GetMessage write SetMessage;
|
||||
property Data: TObject read GetData write SetData;
|
||||
property ObjectDictionary: IMVCObjectDictionary read GetObjectDictionary write SetObjectDictionary;
|
||||
property Headers: TStringList read GetHeaders write SetHeaders;
|
||||
end;
|
||||
|
||||
TMVCRenderer = class(TMVCBase)
|
||||
protected
|
||||
FContext: TWebContext;
|
||||
FContentCharset: string;
|
||||
FResponseStream: TStringBuilder;
|
||||
function ToMVCList(const AObject: TObject; AOwnsObject: Boolean = False): IMVCList;
|
||||
function StatusCodeResponseWithOptionalBody(const StatusCode: Word; const Body: TObject): IMVCResponse;
|
||||
public { this must be public because of entity processors }
|
||||
function GetContentType: string;
|
||||
function GetStatusCode: Integer;
|
||||
@ -679,6 +715,9 @@ type
|
||||
procedure Redirect(const AUrl: string); virtual;
|
||||
procedure ResponseStatus(const AStatusCode: Integer; const AReasonString: string = ''); virtual;
|
||||
class procedure InternalRenderMVCResponse(const Controller: TMVCRenderer; const MVCResponse: TMVCResponse);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// <summary>
|
||||
/// HTTP Status 201 indicates that as a result of HTTP POST request, one or more new resources have been successfully created on server.
|
||||
/// The response may contain URI in Location header field in HTTP headers list, which can have reference to the newly created resource. Also, response payload also may include an entity containing a list of resource characteristics and location(s) from which the user or user agent can choose the one most appropriate.
|
||||
@ -688,7 +727,36 @@ type
|
||||
/// https://restfulapi.net/http-status-201-created/
|
||||
/// </remarks>
|
||||
procedure Render201Created(const Location: string = '';
|
||||
const Reason: string = ''); virtual;
|
||||
const Reason: string = ''); virtual; deprecated;
|
||||
|
||||
|
||||
//Response Result
|
||||
{
|
||||
BadRequestResult
|
||||
ConflictResult
|
||||
NoContentResult
|
||||
NotFoundResult
|
||||
OkResult
|
||||
UnauthorizedResult
|
||||
UnprocessableEntityResult
|
||||
UnsupportedMediaTypeResult
|
||||
ConflictResult
|
||||
InternalServerErrorResult
|
||||
}
|
||||
|
||||
function OKResponse(const Body: TObject): IMVCResponse; overload;
|
||||
function OKResponse: IMVCResponse; overload;
|
||||
function NotFoundResponse(const Body: TObject): IMVCResponse; overload;
|
||||
function NotFoundResponse: IMVCResponse; overload;
|
||||
function NoContentResponse: IMVCResponse;
|
||||
function UnauthorizedResponse: IMVCResponse;
|
||||
function BadRequestResponse: IMVCResponse; overload;
|
||||
function BadRequestResponse(const Error: TObject): IMVCResponse; overload;
|
||||
function CreatedResponse(const Location: string = ''; const Body: TObject = nil): IMVCResponse;
|
||||
function AcceptedResponse(const Location: string = ''; const Body: TObject = nil): IMVCResponse;
|
||||
function ConflictResult: IMVCResponse;
|
||||
function InternalServerErrorResponse: IMVCResponse;
|
||||
|
||||
/// <summary>
|
||||
/// Allow a server to accept a request for some other process (perhaps a batch-oriented process that is only run once per day) without requiring that the user agents connection to the server persist until the process is completed.
|
||||
/// The entity returned with this response SHOULD describe the requests current status and point to (or embed) a status monitor that can provide the user with (or without) an estimate of when the request will be fulfilled.
|
||||
@ -697,13 +765,18 @@ type
|
||||
/// https://restfulapi.net/http-status-202-accepted/
|
||||
/// </remarks>
|
||||
procedure Render202Accepted(const HREF: string; const ID: string;
|
||||
const Reason: string = 'Accepted'); virtual;
|
||||
const Reason: string = 'Accepted'); virtual; deprecated;
|
||||
/// <summary>
|
||||
/// HTTP Status 204 (No Content) indicates that the server has successfully fulfilled the request and that there is no content to send in the response payload body. The server might want to return updated meta information in the form of entity-headers, which if present SHOULD be applied to current documents active view if any.
|
||||
/// The 204 response MUST NOT include a message-body and thus is always terminated by the first empty line after the header fields.
|
||||
/// </summary>
|
||||
procedure Render204NoContent(const Location: string = '';
|
||||
const Reason: string = ''); virtual;
|
||||
const Reason: string = ''); virtual; deprecated;
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
function Serializer: IMVCSerializer; overload;
|
||||
function Serializer(const AContentType: string;
|
||||
const ARaiseExceptionIfNotExists: Boolean = True): IMVCSerializer; overload;
|
||||
@ -962,22 +1035,22 @@ type
|
||||
ALLOWED_TYPED_ACTION_PARAMETERS_TYPES =
|
||||
'Integer, Int64, Single, Double, Extended, Boolean, TDate, TTime, TDateTime, String and TGUID';
|
||||
private
|
||||
FViewEngineClass: TMVCViewEngineClass;
|
||||
FWebModule: TWebModule;
|
||||
FConfig: TMVCConfig;
|
||||
FConfigCache_MaxRequestSize: Int64;
|
||||
FConfigCache_ExposeServerSignature: Boolean;
|
||||
FConfigCache_ServerSignature: string;
|
||||
FConfigCache_ExposeXPoweredBy: Boolean;
|
||||
FConfigCache_DefaultContentType: String;
|
||||
FConfigCache_DefaultContentCharset: String;
|
||||
FConfigCache_PathPrefix: String;
|
||||
FSerializers: TDictionary<string, IMVCSerializer>;
|
||||
FMiddlewares: TList<IMVCMiddleware>;
|
||||
FControllers: TObjectList<TMVCControllerDelegate>;
|
||||
FApplicationSession: TWebApplicationSession;
|
||||
FSavedOnBeforeDispatch: THTTPMethodEvent;
|
||||
FOnException: TMVCExceptionHandlerProc;
|
||||
fViewEngineClass: TMVCViewEngineClass;
|
||||
fWebModule: TWebModule;
|
||||
fConfig: TMVCConfig;
|
||||
fConfigCache_MaxRequestSize: Int64;
|
||||
fConfigCache_ExposeServerSignature: Boolean;
|
||||
fConfigCache_ServerSignature: string;
|
||||
fConfigCache_ExposeXPoweredBy: Boolean;
|
||||
fConfigCache_DefaultContentType: String;
|
||||
fConfigCache_DefaultContentCharset: String;
|
||||
fConfigCache_PathPrefix: String;
|
||||
fSerializers: TDictionary<string, IMVCSerializer>;
|
||||
fMiddlewares: TList<IMVCMiddleware>;
|
||||
fControllers: TObjectList<TMVCControllerDelegate>;
|
||||
fApplicationSession: TWebApplicationSession;
|
||||
fSavedOnBeforeDispatch: THTTPMethodEvent;
|
||||
fOnException: TMVCExceptionHandlerProc;
|
||||
fOnRouterLog: TMVCRouterLogHandlerProc;
|
||||
fWebContextCreateEvent: TWebContextCreateEvent;
|
||||
fWebContextDestroyEvent: TWebContextDestroyEvent;
|
||||
@ -988,6 +1061,9 @@ type
|
||||
function GetViewEngineClass: TMVCViewEngineClass;
|
||||
procedure HandleDefaultValueForInjectedParameter(var InjectedParamValue: String;
|
||||
const InjectableParamAttribute: MVCInjectableParamAttribute);
|
||||
// procedure FillActualParamsForConstructor(
|
||||
// const AActionFormalParams: TArray<TRttiParameter>;
|
||||
// var AActualParams: TArray<TValue>);
|
||||
protected
|
||||
procedure DoWebContextCreateEvent(const AContext: TWebContext); inline;
|
||||
procedure DoWebContextDestroyEvent(const AContext: TWebContext); inline;
|
||||
@ -1014,6 +1090,10 @@ type
|
||||
const AResponse: TWebResponse); virtual;
|
||||
function ExecuteAction(const ASender: TObject; const ARequest: TWebRequest;
|
||||
const AResponse: TWebResponse): Boolean; virtual;
|
||||
function CreateControllerWithDependencies(
|
||||
const Context: TWebContext;
|
||||
const ControllerClass: TMVCControllerClazz;
|
||||
const ConstructorMethod: TRttiMethod): TMVCController;
|
||||
public
|
||||
class function GetCurrentSession(const ASessionId: string;
|
||||
const ARaiseExceptionIfExpired: Boolean = True): TMVCWebSession; static;
|
||||
@ -1054,7 +1134,6 @@ type
|
||||
property ViewEngineClass: TMVCViewEngineClass read GetViewEngineClass;
|
||||
property WebModule: TWebModule read FWebModule;
|
||||
property Config: TMVCConfig read FConfig;
|
||||
//property Serializers: TDictionary<string, IMVCSerializer> read FSerializers;
|
||||
property Middlewares: TList<IMVCMiddleware> read FMiddlewares;
|
||||
property Controllers: TObjectList<TMVCControllerDelegate> read FControllers;
|
||||
property ApplicationSession: TWebApplicationSession read FApplicationSession
|
||||
@ -1074,29 +1153,6 @@ type
|
||||
end;
|
||||
|
||||
|
||||
// std responses
|
||||
|
||||
IMVCResponse = interface
|
||||
['{9DFEC741-EE38-4AC9-9C2C-9EA0D15D08D5}']
|
||||
function GetData: TObject;
|
||||
function GetMessage: string;
|
||||
function GetStatusCode: Integer;
|
||||
function GetHeaders: TStringList;
|
||||
procedure SetData(const Value: TObject);
|
||||
procedure SetMessage(const Value: string);
|
||||
procedure SetHeaders(const Headers: TStringList);
|
||||
procedure SetObjectDictionary(const Value: IMVCObjectDictionary);
|
||||
function GetObjectDictionary: IMVCObjectDictionary;
|
||||
procedure SetStatusCode(const Value: Integer);
|
||||
function GetIgnoredList: TMVCIgnoredList;
|
||||
function HasHeaders: Boolean;
|
||||
function HasBody: Boolean;
|
||||
property StatusCode: Integer read GetStatusCode write SetStatusCode;
|
||||
property Message: string read GetMessage write SetMessage;
|
||||
property Data: TObject read GetData write SetData;
|
||||
property ObjectDictionary: IMVCObjectDictionary read GetObjectDictionary write SetObjectDictionary;
|
||||
property Headers: TStringList read GetHeaders write SetHeaders;
|
||||
end;
|
||||
|
||||
TMVCBaseResponse = class abstract (TInterfacedObject, IMVCResponse)
|
||||
protected
|
||||
@ -2102,10 +2158,11 @@ begin
|
||||
raise EMVCException.Create('Session already bounded for this request');
|
||||
end;
|
||||
|
||||
constructor TWebContext.Create(const ARequest: TWebRequest; const AResponse: TWebResponse;
|
||||
constructor TWebContext.Create(const AServiceContainerResolver: IMVCServiceContainerResolver; const ARequest: TWebRequest; const AResponse: TWebResponse;
|
||||
const AConfig: TMVCConfig; const ASerializers: TDictionary<string, IMVCSerializer>);
|
||||
begin
|
||||
inherited Create;
|
||||
FServiceContainerResolver := AServiceContainerResolver;
|
||||
FIsSessionStarted := False;
|
||||
FSessionMustBeClose := False;
|
||||
FWebSession := nil;
|
||||
@ -2506,6 +2563,65 @@ begin
|
||||
LoadSystemControllers;
|
||||
end;
|
||||
|
||||
|
||||
//procedure TMVCEngine.FillActualParamsForConstructor(
|
||||
// const AActionFormalParams: TArray<TRttiParameter>;
|
||||
// var AActualParams: TArray<TValue>);
|
||||
//var
|
||||
// lParamName: string;
|
||||
// I: Integer;
|
||||
// lIntf, lOutIntf: IInterface;
|
||||
//begin
|
||||
// SetLength(AActualParams, Length(AActionFormalParams));
|
||||
// for I := 0 to Length(AActionFormalParams) - 1 do
|
||||
// begin
|
||||
// lParamName := AActionFormalParams[I].name;
|
||||
// lIntf := fServiceContainer.Resolve(AActionFormalParams[I].ParamType.Handle);
|
||||
// if not Supports(lIntf, AActionFormalParams[I].ParamType.Handle.TypeData.GUID, lOutIntf) then
|
||||
// begin
|
||||
// raise EMVCException.Create('Cannot inject ' + AActionFormalParams[I].Name + ' into constructor ' + );
|
||||
// end;
|
||||
// TValue.Make(@lOutIntf, AActionFormalParams[I].ParamType.Handle, AActualParams[I]);
|
||||
// end;
|
||||
//end;
|
||||
//
|
||||
|
||||
function TMVCEngine.CreateControllerWithDependencies(
|
||||
const Context: TWebContext;
|
||||
const ControllerClass: TMVCControllerClazz;
|
||||
const ConstructorMethod: TRttiMethod): TMVCController;
|
||||
var
|
||||
lActionFormalParams: TArray<TRttiParameter>;
|
||||
lActualParams: TArray<TValue>;
|
||||
I: Integer;
|
||||
lIntf, lOutIntf: IInterface;
|
||||
lInjectAttribute: MVCInjectAttribute;
|
||||
lServiceName: string;
|
||||
begin
|
||||
lActionFormalParams := ConstructorMethod.GetParameters;
|
||||
SetLength(lActualParams, Length(lActionFormalParams));
|
||||
if Length(lActionFormalParams) > 0 then
|
||||
begin
|
||||
for I := 0 to Length(lActionFormalParams) - 1 do
|
||||
begin
|
||||
lServiceName := '';
|
||||
lInjectAttribute := lActionFormalParams[I].GetAttribute<MVCInjectAttribute>;
|
||||
if lInjectAttribute <> nil then
|
||||
begin
|
||||
lServiceName := lInjectAttribute.ServiceName;
|
||||
end;
|
||||
if (lActionFormalParams[I].ParamType.TypeKind <> tkInterface) then
|
||||
begin
|
||||
raise EMVCException.CreateFmt('Parameter "%s" is not an interface type', [lActionFormalParams[i].ToString]);
|
||||
end;
|
||||
lIntf := Context.ServiceContainerResolver.Resolve(lActionFormalParams[I].ParamType.Handle, lServiceName);
|
||||
Supports(lIntf, lActionFormalParams[I].ParamType.Handle.TypeData.GUID, lOutIntf);
|
||||
TValue.Make(@lOutIntf, lActionFormalParams[I].ParamType.Handle, lActualParams[I]);
|
||||
end;
|
||||
end;
|
||||
Result := TMVCController(ConstructorMethod.Invoke(ControllerClass, lActualParams).AsObject);
|
||||
end;
|
||||
|
||||
function TMVCEngine.CustomExceptionHandling(const Ex: Exception;
|
||||
const ASelectedController: TMVCController; const AContext: TWebContext): Boolean;
|
||||
begin
|
||||
@ -2528,10 +2644,10 @@ end;
|
||||
|
||||
destructor TMVCEngine.Destroy;
|
||||
begin
|
||||
FConfig.Free;
|
||||
FSerializers.Free;
|
||||
FMiddlewares.Free;
|
||||
FControllers.Free;
|
||||
fConfig.Free;
|
||||
fSerializers.Free;
|
||||
fMiddlewares.Free;
|
||||
fControllers.Free;
|
||||
inherited Destroy;
|
||||
end;
|
||||
|
||||
@ -2591,7 +2707,7 @@ begin
|
||||
{$ENDIF}
|
||||
lParamsTable := TMVCRequestParamsTable.Create;
|
||||
try
|
||||
lContext := TWebContext.Create(ARequest, AResponse, FConfig, FSerializers);
|
||||
lContext := TWebContext.Create(NewServiceContainerResolver, ARequest, AResponse, FConfig, FSerializers);
|
||||
try
|
||||
DefineDefaultResponseHeaders(lContext);
|
||||
DoWebContextCreateEvent(lContext);
|
||||
@ -2612,17 +2728,28 @@ begin
|
||||
lResponseContentCharset) then
|
||||
begin
|
||||
try
|
||||
if Assigned(lRouter.ControllerCreateAction) then
|
||||
lSelectedController := lRouter.ControllerCreateAction()
|
||||
if lRouter.ControllerCreateAction <> nil then
|
||||
begin
|
||||
lSelectedController := lRouter.ControllerCreateAction();
|
||||
end
|
||||
else if lRouter.ControllerInjectableConstructor <> nil then
|
||||
begin
|
||||
lSelectedController := CreateControllerWithDependencies(
|
||||
lContext,
|
||||
lRouter.ControllerClazz,
|
||||
lRouter.ControllerInjectableConstructor);
|
||||
end
|
||||
else
|
||||
begin
|
||||
lSelectedController := lRouter.ControllerClazz.Create;
|
||||
end;
|
||||
except
|
||||
on Ex: Exception do
|
||||
begin
|
||||
Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
|
||||
[Ex.Classname, Ex.Message, GetRequestShortDescription(ARequest), 'Cannot create controller'], LOGGERPRO_TAG);
|
||||
raise EMVCException.Create(http_status.InternalServerError,
|
||||
'Cannot create controller');
|
||||
'Cannot create controller (see log for more info)');
|
||||
end;
|
||||
end;
|
||||
lRouterMethodToCallName := lRouter.MethodToCall.Name;
|
||||
@ -2984,11 +3111,13 @@ var
|
||||
lFromContentFieldAttribute: MVCFromContentFieldAttribute;
|
||||
lFromHeaderAttribute: MVCFromHeaderAttribute;
|
||||
lFromCookieAttribute: MVCFromCookieAttribute;
|
||||
lInjectAttribute: MVCInjectAttribute;
|
||||
lAttributeInjectedParamCount: Integer;
|
||||
lInjectedParamValue: string;
|
||||
lInjectedMultiParamValue: TArray<String>;
|
||||
lList: IMVCList;
|
||||
lItemClass: TClass;
|
||||
lIntf, lOutIntf: IInterface;
|
||||
begin
|
||||
ABodyParameter := nil;
|
||||
lAttributeInjectedParamCount := 0;
|
||||
@ -3066,11 +3195,19 @@ begin
|
||||
HandleDefaultValueForInjectedParameter(lInjectedParamValue, lFromCookieAttribute);
|
||||
AActualParams[I] := GetActualParam(AActionFormalParams[I], lInjectedParamValue);
|
||||
end
|
||||
else if TRttiUtils.HasAttribute<MVCInjectAttribute>(AActionFormalParams[I],
|
||||
lInjectAttribute) then
|
||||
begin
|
||||
Inc(lAttributeInjectedParamCount, 1);
|
||||
lIntf := AContext.ServiceContainerResolver.Resolve(AActionFormalParams[I].ParamType.Handle, lInjectAttribute.ServiceName);
|
||||
Supports(lIntf, AActionFormalParams[I].ParamType.Handle.TypeData.GUID, lOutIntf);
|
||||
TValue.Make(@lOutIntf, AActionFormalParams[I].ParamType.Handle, AActualParams[I]);
|
||||
end
|
||||
else
|
||||
begin
|
||||
raise EMVCException.Create(http_status.InternalServerError,
|
||||
'Unknown custom attribute on action parameter: ' + AActionFormalParams[I].name +
|
||||
'. [HINT: Allowed attributes are MVCFromBody, MVCFromQueryString, MVCFromHeader, MVCFromCookie]');
|
||||
'. [HINT: Allowed attributes are MVCFromBody, MVCFromQueryString, MVCFromHeader, MVCFromCookie, MVCInject]');
|
||||
end;
|
||||
Continue;
|
||||
end;
|
||||
@ -3804,6 +3941,55 @@ begin
|
||||
'Hint: Messaging extensions require a valid clientid. Did you call /messages/clients/YOUR_CLIENT_ID ?');
|
||||
end;
|
||||
|
||||
function TMVCRenderer.BadRequestResponse: IMVCResponse;
|
||||
begin
|
||||
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.BadRequest, nil);
|
||||
end;
|
||||
|
||||
function TMVCRenderer.AcceptedResponse(const Location: string;
|
||||
const Body: TObject): IMVCResponse;
|
||||
var
|
||||
lRespBuilder: IMVCResponseBuilder;
|
||||
begin
|
||||
lRespBuilder := MVCResponseBuilder;
|
||||
if not Location.IsEmpty then
|
||||
begin
|
||||
lRespBuilder.Header('location', Location)
|
||||
end;
|
||||
if Assigned(Body) then
|
||||
begin
|
||||
lRespBuilder.Body(Body, True);
|
||||
end;
|
||||
Result := lRespBuilder.StatusCode(HTTP_STATUS.Accepted).Build;
|
||||
end;
|
||||
|
||||
function TMVCRenderer.BadRequestResponse(const Error: TObject): IMVCResponse;
|
||||
begin
|
||||
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.BadRequest, Error);
|
||||
end;
|
||||
|
||||
function TMVCRenderer.ConflictResult: IMVCResponse;
|
||||
begin
|
||||
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.Conflict, nil);
|
||||
end;
|
||||
|
||||
function TMVCRenderer.CreatedResponse(const Location: string;
|
||||
const Body: TObject): IMVCResponse;
|
||||
var
|
||||
lRespBuilder: IMVCResponseBuilder;
|
||||
begin
|
||||
lRespBuilder := MVCResponseBuilder;
|
||||
if not Location.IsEmpty then
|
||||
begin
|
||||
lRespBuilder.Header('location', Location)
|
||||
end;
|
||||
if Assigned(Body) then
|
||||
begin
|
||||
lRespBuilder.Body(Body, True);
|
||||
end;
|
||||
Result := lRespBuilder.StatusCode(HTTP_STATUS.Created).Build;
|
||||
end;
|
||||
|
||||
function TMVCRenderer.GetContentType: string;
|
||||
begin
|
||||
Result := GetContext.Response.ContentType.Trim;
|
||||
@ -3892,6 +4078,36 @@ end;
|
||||
|
||||
end;
|
||||
|
||||
function TMVCRenderer.InternalServerErrorResponse: IMVCResponse;
|
||||
begin
|
||||
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.InternalServerError, nil);
|
||||
end;
|
||||
|
||||
function TMVCRenderer.NoContentResponse: IMVCResponse;
|
||||
begin
|
||||
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.NoContent, nil);
|
||||
end;
|
||||
|
||||
function TMVCRenderer.NotFoundResponse: IMVCResponse;
|
||||
begin
|
||||
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.NotFound, nil);
|
||||
end;
|
||||
|
||||
function TMVCRenderer.NotFoundResponse(const Body: TObject): IMVCResponse;
|
||||
begin
|
||||
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.NotFound, Body);
|
||||
end;
|
||||
|
||||
function TMVCRenderer.OKResponse: IMVCResponse;
|
||||
begin
|
||||
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.OK, nil);
|
||||
end;
|
||||
|
||||
function TMVCRenderer.OKResponse(const Body: TObject): IMVCResponse;
|
||||
begin
|
||||
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.OK, Body);
|
||||
end;
|
||||
|
||||
function TMVCController.GetViewData(const aModelName: string): TValue;
|
||||
begin
|
||||
if not FViewModel.TryGetValue(aModelName, Result) then
|
||||
@ -4176,11 +4392,28 @@ begin
|
||||
GetContext.Response.StatusCode := AValue;
|
||||
end;
|
||||
|
||||
function TMVCRenderer.StatusCodeResponseWithOptionalBody(const StatusCode: Word; const Body: TObject): IMVCResponse;
|
||||
begin
|
||||
if Body = nil then
|
||||
begin
|
||||
Result := MVCResponseBuilder.StatusCode(StatusCode).Build;
|
||||
end
|
||||
else
|
||||
begin
|
||||
Result := MVCResponseBuilder.StatusCode(StatusCode).Body(Body, True).Build;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TMVCRenderer.ToMVCList(const AObject: TObject; AOwnsObject: Boolean): IMVCList;
|
||||
begin
|
||||
Result := MVCFramework.DuckTyping.WrapAsList(AObject, AOwnsObject);
|
||||
end;
|
||||
|
||||
function TMVCRenderer.UnauthorizedResponse: IMVCResponse;
|
||||
begin
|
||||
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.Unauthorized, nil);
|
||||
end;
|
||||
|
||||
procedure TMVCController.SetETag(const Data: String);
|
||||
begin
|
||||
Context.Response.SetCustomHeader('ETag', GetSHA1HashFromString(Data));
|
||||
@ -4961,6 +5194,16 @@ begin
|
||||
inherited;
|
||||
end;
|
||||
|
||||
{ MVCInjectAttribute }
|
||||
|
||||
{ MVCInjectAttribute }
|
||||
|
||||
constructor MVCInjectAttribute.Create(ServiceName: String);
|
||||
begin
|
||||
inherited Create;
|
||||
fServiceName := ServiceName;
|
||||
end;
|
||||
|
||||
initialization
|
||||
|
||||
// https://quality.embarcadero.com/browse/RSP-38281
|
||||
|
@ -100,3 +100,7 @@ DelphiMVCFramework is compatible with Delphi version XE7 or better
|
||||
{$ENDIF}
|
||||
{$DEFINE USEFIREDAC}
|
||||
|
||||
{$IF Defined(SYDNEYORBETTER)}
|
||||
{$DEFINE CUSTOM_MANAGED_RECORDS}
|
||||
{$ENDIF}
|
||||
|
||||
|
@ -676,6 +676,34 @@ type
|
||||
property BackEndName: String read FBackEndName write SetBackEndName;
|
||||
end;
|
||||
|
||||
|
||||
|
||||
// TEST INJECTOR
|
||||
IMyInterface1 = interface
|
||||
['{AA4EFC41-F34F-4B50-AC3B-5627D4C48CE2}']
|
||||
function MyMethod1: String;
|
||||
end;
|
||||
|
||||
IMyInterface2 = interface
|
||||
['{3FE46150-81CA-4ACD-BA8D-B94D1492B1E6}']
|
||||
function MyMethod2: String;
|
||||
end;
|
||||
|
||||
IMyInterface3 = interface
|
||||
['{7A4ECD36-3B81-4C87-85CE-1C3AFBD7718F}']
|
||||
function MyMethod3: String;
|
||||
end;
|
||||
|
||||
TMyService = class(TInterfacedObject, IMyInterface1, IMyInterface2)
|
||||
function MyMethod1: String;
|
||||
function MyMethod2: String;
|
||||
end;
|
||||
|
||||
TMyService2 = class(TInterfacedObject, IMyInterface3)
|
||||
function MyMethod3: String;
|
||||
end;
|
||||
|
||||
|
||||
function GetMyObject: TMyObject;
|
||||
function GetMyObjectWithTValue: TMyObjectWithTValue;
|
||||
function GetMyObjectWithStream: TMyStreamObject;
|
||||
@ -1454,6 +1482,25 @@ begin
|
||||
FBackEndName := Value;
|
||||
end;
|
||||
|
||||
{ TMyService }
|
||||
|
||||
function TMyService.MyMethod1: String;
|
||||
begin
|
||||
Result := 'TMyService.MyMethod1';
|
||||
end;
|
||||
|
||||
function TMyService.MyMethod2: String;
|
||||
begin
|
||||
Result := 'TMyService.MyMethod2';
|
||||
end;
|
||||
|
||||
{ TMyService2 }
|
||||
|
||||
function TMyService2.MyMethod3: String;
|
||||
begin
|
||||
Result := 'TMyService2.MyMethod3';
|
||||
end;
|
||||
|
||||
initialization
|
||||
|
||||
ActiveRecordMappingRegistry.AddEntity('customers', TCustomer);
|
||||
|
@ -76,7 +76,9 @@ uses
|
||||
IntfObjectPoolTestU in 'IntfObjectPoolTestU.pas',
|
||||
ObjectPoolTestU in 'ObjectPoolTestU.pas',
|
||||
MVCFramework.DotEnv.Parser in '..\..\..\sources\MVCFramework.DotEnv.Parser.pas',
|
||||
MVCFramework.DotEnv in '..\..\..\sources\MVCFramework.DotEnv.pas';
|
||||
MVCFramework.DotEnv in '..\..\..\sources\MVCFramework.DotEnv.pas',
|
||||
InjectorTestU in 'InjectorTestU.pas',
|
||||
MVCFramework.Container in '..\..\..\sources\MVCFramework.Container.pas';
|
||||
|
||||
{$R *.RES}
|
||||
|
||||
|
@ -274,6 +274,8 @@
|
||||
<DCCReference Include="ObjectPoolTestU.pas"/>
|
||||
<DCCReference Include="..\..\..\sources\MVCFramework.DotEnv.Parser.pas"/>
|
||||
<DCCReference Include="..\..\..\sources\MVCFramework.DotEnv.pas"/>
|
||||
<DCCReference Include="InjectorTestU.pas"/>
|
||||
<DCCReference Include="..\..\..\sources\MVCFramework.Container.pas"/>
|
||||
<BuildConfiguration Include="Base">
|
||||
<Key>Base</Key>
|
||||
</BuildConfiguration>
|
||||
@ -366,6 +368,18 @@
|
||||
<DeployFile LocalName="$(BDS)\Redist\osx32\libcgunwind.1.0.dylib" Class="DependencyModule"/>
|
||||
<DeployFile LocalName="Win32\Debug\DMVCFrameworkTests.exe" Configuration="Debug" Class="ProjectOutput"/>
|
||||
<DeployFile LocalName="Win32\Debug\DMVCFrameworkTests.rsm" Configuration="Debug" Class="DebugSymbols"/>
|
||||
<DeployFile LocalName="bin32\DMVCFrameworkTests.exe" Configuration="TESTINSIGHT" Class="ProjectOutput">
|
||||
<Platform Name="Win32">
|
||||
<RemoteName>DMVCFrameworkTests.exe</RemoteName>
|
||||
<Overwrite>true</Overwrite>
|
||||
</Platform>
|
||||
</DeployFile>
|
||||
<DeployFile LocalName="bin32\DMVCFrameworkTests.rsm" Configuration="TESTINSIGHT" Class="DebugSymbols">
|
||||
<Platform Name="Win32">
|
||||
<RemoteName>DMVCFrameworkTests.rsm</RemoteName>
|
||||
<Overwrite>true</Overwrite>
|
||||
</Platform>
|
||||
</DeployFile>
|
||||
<DeployFile LocalName="bin64\DMVCFrameworkTests.exe" Configuration="TESTINSIGHT" Class="ProjectOutput">
|
||||
<Platform Name="Win64">
|
||||
<RemoteName>DMVCFrameworkTests.exe</RemoteName>
|
||||
@ -381,21 +395,9 @@
|
||||
<DeployFile LocalName="bin\DMVCFrameworkTests.exe" Configuration="CONSOLE" Class="ProjectOutput"/>
|
||||
<DeployFile LocalName="bin\DMVCFrameworkTests.exe" Configuration="GUI" Class="ProjectOutput"/>
|
||||
<DeployFile LocalName="bin\DMVCFrameworkTests.exe" Configuration="TESTINSIGHT" Class="ProjectOutput"/>
|
||||
<DeployFile LocalName="bin\DMVCFrameworkTests.exe" Configuration="TESTINSIGHT" Class="ProjectOutput">
|
||||
<Platform Name="Win32">
|
||||
<RemoteName>DMVCFrameworkTests.exe</RemoteName>
|
||||
<Overwrite>true</Overwrite>
|
||||
</Platform>
|
||||
</DeployFile>
|
||||
<DeployFile LocalName="bin\DMVCFrameworkTests.rsm" Configuration="CONSOLE" Class="DebugSymbols"/>
|
||||
<DeployFile LocalName="bin\DMVCFrameworkTests.rsm" Configuration="GUI" Class="DebugSymbols"/>
|
||||
<DeployFile LocalName="bin\DMVCFrameworkTests.rsm" Configuration="TESTINSIGHT" Class="DebugSymbols"/>
|
||||
<DeployFile LocalName="bin\DMVCFrameworkTests.rsm" Configuration="TESTINSIGHT" Class="DebugSymbols">
|
||||
<Platform Name="Win32">
|
||||
<RemoteName>DMVCFrameworkTests.rsm</RemoteName>
|
||||
<Overwrite>true</Overwrite>
|
||||
</Platform>
|
||||
</DeployFile>
|
||||
<DeployClass Name="AdditionalDebugSymbols">
|
||||
<Platform Name="OSX32">
|
||||
<Operation>1</Operation>
|
||||
|
@ -288,7 +288,6 @@ type
|
||||
procedure TestInLineComments;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
{$WARN SYMBOL_DEPRECATED OFF}
|
||||
@ -2396,6 +2395,8 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
|
||||
initialization
|
||||
|
||||
TDUnitX.RegisterTestFixture(TTestRouting);
|
||||
|
254
unittests/general/Several/InjectorTestU.pas
Normal file
254
unittests/general/Several/InjectorTestU.pas
Normal file
@ -0,0 +1,254 @@
|
||||
// ***************************************************************************
|
||||
//
|
||||
// Delphi MVC Framework
|
||||
//
|
||||
// Copyright (c) 2010-2024 Daniele Teti and the DMVCFramework Team
|
||||
//
|
||||
// https://github.com/danieleteti/delphimvcframework
|
||||
//
|
||||
// The Initial Developer of the Original Code is Vivaticket S.p.A. https://www.vivaticket.com/
|
||||
// The code has been fully donated to the DMVCFramework community without any charge nor rights.
|
||||
//
|
||||
// ***************************************************************************
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// ***************************************************************************
|
||||
|
||||
unit InjectorTestU;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
DUnitX.TestFramework, MVCFramework.Container,
|
||||
MVCFramework.Serializer.JsonDataObjects;
|
||||
|
||||
type
|
||||
|
||||
[TestFixture]
|
||||
TTestContainer = class
|
||||
public
|
||||
[Test]
|
||||
procedure TestNotBuiltContainer;
|
||||
[Test]
|
||||
procedure TestUnknownService;
|
||||
[Test]
|
||||
procedure TestTransient;
|
||||
[Test]
|
||||
procedure TestSingleton;
|
||||
[Test]
|
||||
procedure TestSingletonPerRequest;
|
||||
[Test]
|
||||
procedure TestCascadeConstructorInjection;
|
||||
end;
|
||||
|
||||
|
||||
IServiceA = interface
|
||||
['{B6C5EAD8-9008-4200-BF33-E3DE5C8A2320}']
|
||||
end;
|
||||
|
||||
IServiceB = interface
|
||||
['{8418244D-8AEC-4567-A21E-3F4ECD07E227}']
|
||||
end;
|
||||
|
||||
IServiceC = interface
|
||||
['{A9E5FD77-87FD-4C9C-91BA-79556252DAAD}']
|
||||
function GetServiceA: IServiceA;
|
||||
function GetServiceB: IServiceB;
|
||||
end;
|
||||
|
||||
TServiceA = class(TInterfacedObject, IServiceA)
|
||||
|
||||
end;
|
||||
|
||||
TServiceB = class(TInterfacedObject, IServiceB)
|
||||
|
||||
end;
|
||||
|
||||
TServiceAB = class(TInterfacedObject, IServiceA, IServiceB)
|
||||
|
||||
end;
|
||||
|
||||
TServiceC = class(TInterfacedObject, IServiceC)
|
||||
private
|
||||
fServiceA: IServiceA;
|
||||
fServiceB: IServiceB;
|
||||
protected
|
||||
function GetServiceA: IServiceA;
|
||||
function GetServiceB: IServiceB;
|
||||
public
|
||||
constructor Create(ServiceA: IServiceA; ServiceB: IServiceB);
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.Generics.Collections, MVCFramework.IntfObjectPool, System.SysUtils, System.Classes, SyncObjs,
|
||||
MVCFramework.Serializer.Intf;
|
||||
|
||||
{ TTestContainer }
|
||||
|
||||
procedure TTestContainer.TestCascadeConstructorInjection;
|
||||
begin
|
||||
var lCont := NewMVCServiceContainer;
|
||||
lCont.RegisterType(TServiceA, IServiceA);
|
||||
lCont.RegisterType(TServiceB, IServiceB, '', TRegistrationType.SingletonPerRequest);
|
||||
lCont.RegisterType(TServiceC, IServiceC);
|
||||
lCont.Build;
|
||||
|
||||
// 1° "request"
|
||||
var lResolver := NewServiceContainerResolver(lCont);
|
||||
var l0 := lResolver.Resolve(TypeInfo(IServiceC)) as IServiceC;
|
||||
Assert.IsNotNull(l0.GetServiceA);
|
||||
Assert.IsNotNull(l0.GetServiceB);
|
||||
|
||||
// resolve another "IServiceC" in the same request - ServiceB is rtSingletonPerRequest
|
||||
var l01 := lResolver.Resolve(TypeInfo(IServiceC)) as IServiceC;
|
||||
Assert.IsNotNull(l0.GetServiceA);
|
||||
Assert.IsNotNull(l0.GetServiceB);
|
||||
Assert.AreNotEqual(l0.GetServiceA, l01.GetServiceA);
|
||||
Assert.AreEqual(l0.GetServiceB, l01.GetServiceB);
|
||||
|
||||
// 2° "request"
|
||||
lResolver := NewServiceContainerResolver(lCont);
|
||||
var l1 := lResolver.Resolve(TypeInfo(IServiceC)) as IServiceC;
|
||||
Assert.IsNotNull(l0.GetServiceA);
|
||||
Assert.IsNotNull(l0.GetServiceB);
|
||||
Assert.AreNotEqual(l0.GetServiceA, l1.GetServiceA);
|
||||
Assert.AreNotEqual(l0.GetServiceB, l1.GetServiceB);
|
||||
end;
|
||||
|
||||
procedure TTestContainer.TestNotBuiltContainer;
|
||||
begin
|
||||
var lCont := NewMVCServiceContainer;
|
||||
lCont.RegisterType(TServiceA, IServiceA);
|
||||
var lResolver := NewServiceContainerResolver(lCont);
|
||||
Assert.WillRaise(
|
||||
procedure
|
||||
begin
|
||||
var l0 := lResolver.Resolve(TypeInfo(IServiceA));
|
||||
end, EMVCContainerError);
|
||||
end;
|
||||
|
||||
procedure TTestContainer.TestSingleton;
|
||||
begin
|
||||
var lCont := NewMVCServiceContainer;
|
||||
lCont.RegisterType(TServiceA, IServiceA, '', TRegistrationType.Singleton);
|
||||
lCont.RegisterType(TServiceA, IServiceA, 'Svc1', TRegistrationType.Singleton);
|
||||
lCont.Build;
|
||||
|
||||
// 1° Request
|
||||
var lResolver := NewServiceContainerResolver(lCont);
|
||||
var l0 := lResolver.Resolve(TypeInfo(IServiceA));
|
||||
var l1 := lResolver.Resolve(TypeInfo(IServiceA));
|
||||
Assert.AreEqual(l0, l1);
|
||||
var l2 := lResolver.Resolve(TypeInfo(IServiceA), 'Svc1');
|
||||
var l3 := lResolver.Resolve(TypeInfo(IServiceA), 'Svc1');
|
||||
Assert.AreEqual(l2, l3);
|
||||
|
||||
// 2° Request
|
||||
lResolver := NewServiceContainerResolver(lCont);
|
||||
var l10 := lResolver.Resolve(TypeInfo(IServiceA));
|
||||
var l11 := lResolver.Resolve(TypeInfo(IServiceA));
|
||||
Assert.AreEqual(l10, l11);
|
||||
Assert.AreEqual(l0, l10);
|
||||
Assert.AreEqual(l1, l11);
|
||||
end;
|
||||
|
||||
procedure TTestContainer.TestSingletonPerRequest;
|
||||
begin
|
||||
var lCont := NewMVCServiceContainer
|
||||
.RegisterType(TServiceA, IServiceA, '', TRegistrationType.SingletonPerRequest)
|
||||
.RegisterType(TServiceA, IServiceA, 'Svc1', TRegistrationType.SingletonPerRequest);
|
||||
lCont.Build;
|
||||
|
||||
// 1° "request"
|
||||
var lResolver := NewServiceContainerResolver(lCont);
|
||||
var l0 := lResolver.Resolve(TypeInfo(IServiceA));
|
||||
var l1 := lResolver.Resolve(TypeInfo(IServiceA));
|
||||
Assert.AreEqual(l0, l1);
|
||||
var l2 := lResolver.Resolve(TypeInfo(IServiceA), 'Svc1');
|
||||
var l3 := lResolver.Resolve(TypeInfo(IServiceA), 'Svc1');
|
||||
Assert.AreEqual(l2, l3);
|
||||
|
||||
// 2° "request"
|
||||
lResolver := NewServiceContainerResolver(lCont);
|
||||
var l00 := lResolver.Resolve(TypeInfo(IServiceA));
|
||||
var l10 := lResolver.Resolve(TypeInfo(IServiceA));
|
||||
Assert.AreEqual(l00, l10);
|
||||
Assert.AreNotEqual(l0, l00);
|
||||
Assert.AreNotEqual(l1, l10);
|
||||
end;
|
||||
|
||||
|
||||
procedure TTestContainer.TestTransient;
|
||||
begin
|
||||
var lCont := NewMVCServiceContainer;
|
||||
lCont.RegisterType(TServiceA, IServiceA);
|
||||
lCont.RegisterType(TServiceA, IServiceA, 'Svc1');
|
||||
lCont.Build;
|
||||
var lResolver := NewServiceContainerResolver(lCont);
|
||||
var l0 := lResolver.Resolve(TypeInfo(IServiceA));
|
||||
var l1 := lResolver.Resolve(TypeInfo(IServiceA));
|
||||
Assert.AreNotEqual(l0, l1);
|
||||
var l2 := lResolver.Resolve(TypeInfo(IServiceA), 'Svc1');
|
||||
var l3 := lResolver.Resolve(TypeInfo(IServiceA), 'Svc1');
|
||||
Assert.AreNotEqual(l2, l3);
|
||||
end;
|
||||
|
||||
procedure TTestContainer.TestUnknownService;
|
||||
begin
|
||||
var lCont := NewMVCServiceContainer;
|
||||
Assert.WillRaise(
|
||||
procedure
|
||||
begin
|
||||
lCont.RegisterType(TServiceA, IServiceB);
|
||||
end, EMVCContainerErrorUnknownService);
|
||||
|
||||
Assert.WillRaise(
|
||||
procedure
|
||||
begin
|
||||
lCont.RegisterType(TMVCJsonDataObjectsSerializer, IServiceB);
|
||||
end, EMVCContainerErrorUnknownService);
|
||||
|
||||
Assert.WillRaise(
|
||||
procedure
|
||||
begin
|
||||
lCont.RegisterType(TServiceA, IMVCSerializer);
|
||||
end, EMVCContainerErrorUnknownService);
|
||||
end;
|
||||
|
||||
{ TServiceC }
|
||||
|
||||
constructor TServiceC.Create(ServiceA: IServiceA; ServiceB: IServiceB);
|
||||
begin
|
||||
inherited Create;
|
||||
fServiceA := ServiceA;
|
||||
fServiceB := ServiceB;
|
||||
end;
|
||||
|
||||
function TServiceC.GetServiceA: IServiceA;
|
||||
begin
|
||||
Result := fServiceA;
|
||||
end;
|
||||
|
||||
function TServiceC.GetServiceB: IServiceB;
|
||||
begin
|
||||
Result := fServiceB;
|
||||
end;
|
||||
|
||||
initialization
|
||||
|
||||
TDUnitX.RegisterTestFixture(TTestContainer);
|
||||
|
||||
end.
|
Loading…
Reference in New Issue
Block a user