delphimvcframework/sources/MVCFramework.Router.pas

614 lines
19 KiB
ObjectPascal
Raw Normal View History

// ***************************************************************************
//
// Delphi MVC Framework
//
2024-01-02 17:04:27 +01:00
// Copyright (c) 2010-2024 Daniele Teti and the DMVCFramework Team
//
// https://github.com/danieleteti/delphimvcframework
//
2022-03-22 12:38:57 +01:00
// Collaborators on this file: Ezequiel Juliano Müller (ezequieljuliano@gmail.com)
//
// ***************************************************************************
//
// 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.
//
// *************************************************************************** }
2015-12-22 12:38:17 +01:00
2013-10-30 00:48:23 +01:00
unit MVCFramework.Router;
{$I dmvcframework.inc}
2013-10-30 00:48:23 +01:00
interface
uses
System.Rtti,
System.SysUtils,
System.Generics.Collections,
System.RegularExpressions,
2013-10-30 00:48:23 +01:00
MVCFramework,
MVCFramework.Commons,
IdURI, System.Classes;
2013-10-30 00:48:23 +01:00
type
2017-06-02 00:10:31 +02:00
TMVCActionParamCacheItem = class
private
FValue: string;
FParams: TList<TPair<String, String>>;
2024-04-19 13:21:45 +02:00
FRegEx: TRegEx;
2017-06-02 00:10:31 +02:00
public
constructor Create(aValue: string; aParams: TList<TPair<String, String>>); virtual;
2017-06-02 00:10:31 +02:00
destructor Destroy; override;
function Value: string;
function Params: TList<TPair<String, String>>; // this should be read-only...
2024-04-19 13:21:45 +02:00
function Match(const Value: String): TMatch; inline;
2017-06-02 00:10:31 +02:00
end;
TMVCRouter = class(TMVCCustomRouter)
2013-10-30 00:48:23 +01:00
private
FRttiContext: TRttiContext;
FConfig: TMVCConfig;
FMethodToCall: TRttiMethod;
FControllerClazz: TMVCControllerClazz;
FControllerCreateAction: TMVCControllerCreateAction;
FControllerInjectableConstructor: TRttiMethod;
2017-06-02 00:10:31 +02:00
FActionParamsCache: TMVCStringObjectDictionary<TMVCActionParamCacheItem>;
function GetAttribute<T: TCustomAttribute>(const AAttributes: TArray<TCustomAttribute>): T;
function GetFirstMediaType(const AContentType: string): string;
function IsHTTPContentTypeCompatible(
const ARequestMethodType: TMVCHTTPMethodType;
var AContentType: string;
const AAttributes: TArray<TCustomAttribute>): Boolean;
function IsHTTPAcceptCompatible(
const ARequestMethodType: TMVCHTTPMethodType;
var AAccept: string;
const AAttributes: TArray<TCustomAttribute>): Boolean;
function IsHTTPMethodCompatible(
const AMethodType: TMVCHTTPMethodType;
const AAttributes: TArray<TCustomAttribute>): Boolean;
function IsCompatiblePath(
const AMVCPath: string;
const APath: string;
2020-09-16 15:56:14 +02:00
var aParams: TMVCRequestParamsTable): Boolean;
function GetParametersNames(const V: string): TList<TPair<string, string>>;
2013-10-30 00:48:23 +01:00
protected
procedure FillControllerMappedPaths(
2017-06-02 00:10:31 +02:00
const aControllerName: string;
const aControllerAttributes: TArray<TCustomAttribute>;
const aControllerMappedPaths: TStringList);
public
2020-09-16 15:56:14 +02:00
class function StringMethodToHTTPMetod(const aValue: string): TMVCHTTPMethodType; static;
constructor Create(const aConfig: TMVCConfig;
const aActionParamsCache: TMVCStringObjectDictionary<TMVCActionParamCacheItem>);
destructor Destroy; override;
function ExecuteRouting(const ARequestPathInfo: string;
const ARequestMethodType: TMVCHTTPMethodType;
const ARequestContentType, ARequestAccept: string;
const AControllers: TObjectList<TMVCControllerDelegate>;
const ADefaultContentType: string;
const ADefaultContentCharset: string;
const APathPrefix: string;
var ARequestParams: TMVCRequestParamsTable;
out AResponseContentMediaType: string;
out AResponseContentCharset: string): Boolean;
function GetQualifiedActionName: string; override;
property MethodToCall: TRttiMethod read FMethodToCall;
property ControllerClazz: TMVCControllerClazz read FControllerClazz;
property ControllerCreateAction: TMVCControllerCreateAction read FControllerCreateAction;
property ControllerInjectableConstructor: TRttiMethod read FControllerInjectableConstructor;
2013-10-30 00:48:23 +01:00
end;
implementation
uses
System.TypInfo,
2024-03-25 00:15:50 +01:00
System.NetEncoding, MVCFramework.Rtti.Utils, MVCFramework.Container;
2013-10-30 00:48:23 +01:00
{ TMVCRouter }
2020-09-16 15:56:14 +02:00
constructor TMVCRouter.Create(const aConfig: TMVCConfig;
const aActionParamsCache: TMVCStringObjectDictionary<TMVCActionParamCacheItem>);
2013-10-30 00:48:23 +01:00
begin
inherited Create;
FRttiContext := TRttiContext.Create;
2020-09-16 15:56:14 +02:00
FConfig := aConfig;
FMethodToCall := nil;
FControllerClazz := nil;
FControllerCreateAction := nil;
2017-06-02 00:10:31 +02:00
FActionParamsCache := aActionParamsCache;
end;
destructor TMVCRouter.Destroy;
begin
FRttiContext.Free;
inherited Destroy;
2013-10-30 00:48:23 +01:00
end;
function TMVCRouter.ExecuteRouting(const ARequestPathInfo: string;
const ARequestMethodType: TMVCHTTPMethodType;
const ARequestContentType, ARequestAccept: string;
const AControllers: TObjectList<TMVCControllerDelegate>;
const ADefaultContentType: string;
const ADefaultContentCharset: string;
const APathPrefix: string;
var ARequestParams: TMVCRequestParamsTable;
out AResponseContentMediaType: string;
out AResponseContentCharset: string): Boolean;
2013-10-30 00:48:23 +01:00
var
LRequestPathInfo: string;
LRequestAccept: string;
LRequestContentType: string;
LControllerMappedPath: string;
LControllerMappedPaths: TStringList;
LControllerDelegate: TMVCControllerDelegate;
LAttributes: TArray<TCustomAttribute>;
LAtt: TCustomAttribute;
LRttiType: TRttiType;
2020-09-16 15:56:14 +02:00
LMethods: TArray<TRttiMethod>;
LMethod: TRttiMethod;
LMethodPath: string;
LProduceAttribute: MVCProducesAttribute;
2017-09-24 19:40:40 +02:00
lURLSegment: string;
LItem: String;
2020-09-16 15:56:14 +02:00
// JUST FOR DEBUG
// lMethodCompatible: Boolean;
// lContentTypeCompatible: Boolean;
// lAcceptCompatible: Boolean;
2013-10-30 00:48:23 +01:00
begin
Result := False;
2013-10-30 00:48:23 +01:00
FMethodToCall := nil;
FControllerClazz := nil;
FControllerCreateAction := nil;
LRequestAccept := ARequestAccept;
LRequestContentType := ARequestContentType;
LRequestPathInfo := ARequestPathInfo;
if (Trim(LRequestPathInfo) = EmptyStr) then
LRequestPathInfo := '/'
2013-10-30 00:48:23 +01:00
else
begin
2019-05-02 17:38:57 +02:00
if not LRequestPathInfo.StartsWith('/') then
begin
LRequestPathInfo := '/' + LRequestPathInfo;
2019-05-02 17:38:57 +02:00
end;
2013-10-30 00:48:23 +01:00
end;
2022-03-22 12:38:57 +01:00
LRequestPathInfo := TIdURI.PathEncode(Trim(LRequestPathInfo)); //regression introduced in fix for issue 492
2013-10-30 00:48:23 +01:00
2019-05-08 20:20:14 +02:00
TMonitor.Enter(gLock);
2014-04-01 00:02:31 +02:00
try
LControllerMappedPaths := TStringList.Create;
try
for LControllerDelegate in AControllers do
2019-05-02 17:38:57 +02:00
begin
LControllerMappedPaths.Clear;
SetLength(LAttributes, 0);
LRttiType := FRttiContext.GetType(LControllerDelegate.Clazz.ClassInfo);
2013-10-30 00:48:23 +01:00
lURLSegment := LControllerDelegate.URLSegment;
if lURLSegment.IsEmpty then
begin
LAttributes := LRttiType.GetAttributes;
if (LAttributes = nil) then
Continue;
FillControllerMappedPaths(LRttiType.Name, LAttributes, LControllerMappedPaths);
end
else
begin
LControllerMappedPaths.Add(lURLSegment);
end;
2014-04-01 00:02:31 +02:00
for LItem in LControllerMappedPaths do
begin
LControllerMappedPath := LItem;
if (LControllerMappedPath = '/') then
begin
LControllerMappedPath := '';
end;
{$IF defined(TOKYOORBETTER)}
if not LRequestPathInfo.StartsWith(APathPrefix + LControllerMappedPath, True) then
{$ELSE}
if not TMVCStringHelper.StartsWith(APathPrefix + LControllerMappedPath, LRequestPathInfo, True) then
{$ENDIF}
begin
Continue;
end;
LMethods := LRttiType.GetMethods; { do not use GetDeclaredMethods because JSON-RPC rely on this!! }
for LMethod in LMethods do
2019-05-02 17:38:57 +02:00
begin
if LMethod.Visibility <> mvPublic then // 2020-08-08
Continue;
if not (LMethod.MethodKind in [mkProcedure, mkFunction]) then
Continue;
LAttributes := LMethod.GetAttributes;
if Length(LAttributes) = 0 then
Continue;
for LAtt in LAttributes do
2013-10-30 00:48:23 +01:00
begin
if LAtt is MVCPathAttribute then
2013-11-08 23:10:25 +01:00
begin
// THIS BLOCK IS HERE JUST FOR DEBUG
// if LMethod.Name.Contains('GetProject') then
// begin
// lMethodCompatible := True; //debug here
// end;
// lMethodCompatible := IsHTTPMethodCompatible(ARequestMethodType, LAttributes);
// lContentTypeCompatible := IsHTTPContentTypeCompatible(ARequestMethodType, LRequestContentType, LAttributes);
// lAcceptCompatible := IsHTTPAcceptCompatible(ARequestMethodType, LRequestAccept, LAttributes);
if IsHTTPMethodCompatible(ARequestMethodType, LAttributes) and
IsHTTPContentTypeCompatible(ARequestMethodType, LRequestContentType, LAttributes) and
IsHTTPAcceptCompatible(ARequestMethodType, LRequestAccept, LAttributes) then
2014-04-01 00:02:31 +02:00
begin
LMethodPath := MVCPathAttribute(LAtt).Path;
if IsCompatiblePath(APathPrefix + LControllerMappedPath + LMethodPath,
LRequestPathInfo, ARequestParams) then
begin
FMethodToCall := LMethod;
FControllerClazz := LControllerDelegate.Clazz;
FControllerCreateAction := LControllerDelegate.CreateAction;
FControllerInjectableConstructor := nil;
2024-03-27 00:10:48 +01:00
// select the constructor with the most mumber of parameters
if not Assigned(FControllerCreateAction) then
begin
FControllerInjectableConstructor := TRttiUtils.GetConstructorWithAttribute<MVCInjectAttribute>(LRttiType);
end;
2024-03-27 00:10:48 +01:00
// end - select the constructor with the most mumber of parameters
LProduceAttribute := GetAttribute<MVCProducesAttribute>(LAttributes);
if LProduceAttribute <> nil then
begin
AResponseContentMediaType := LProduceAttribute.Value;
AResponseContentCharset := LProduceAttribute.Charset;
end
else
begin
AResponseContentMediaType := ADefaultContentType;
AResponseContentCharset := ADefaultContentCharset;
end;
Exit(True);
end;
2014-04-01 00:02:31 +02:00
end;
end; // if MVCPathAttribute
end; // for in Attributes
end; // for in Methods
end;
end; // for in Controllers
finally
LControllerMappedPaths.Free;
end;
2014-04-01 00:02:31 +02:00
finally
2019-05-08 20:20:14 +02:00
TMonitor.Exit(gLock);
2013-10-30 00:48:23 +01:00
end;
end;
function TMVCRouter.GetAttribute<T>(const AAttributes: TArray<TCustomAttribute>): T;
2013-10-30 00:48:23 +01:00
var
Att: TCustomAttribute;
2013-10-30 00:48:23 +01:00
begin
Result := nil;
for Att in AAttributes do
if Att is T then
Exit(T(Att));
2013-10-30 00:48:23 +01:00
end;
procedure TMVCRouter.FillControllerMappedPaths(
const aControllerName: string;
const aControllerAttributes: TArray<TCustomAttribute>;
const aControllerMappedPaths: TStringList);
2017-06-02 00:10:31 +02:00
var
LFound: Boolean;
LAtt: TCustomAttribute;
begin
LFound := False;
for LAtt in aControllerAttributes do
begin
if LAtt is MVCPathAttribute then
begin
LFound := True;
aControllerMappedPaths.Add(MVCPathAttribute(LAtt).Path);
2017-06-02 00:10:31 +02:00
end;
end;
if not LFound then
begin
2017-06-02 00:10:31 +02:00
raise EMVCException.CreateFmt('Controller %s does not have MVCPath attribute', [aControllerName]);
end;
2017-06-02 00:10:31 +02:00
end;
function TMVCRouter.GetFirstMediaType(const AContentType: string): string;
2014-06-27 15:30:39 +02:00
begin
Result := AContentType;
while Pos(',', Result) > 0 do
Result := Copy(Result, 1, Pos(',', Result) - 1);
while Pos(';', Result) > 0 do
2014-10-26 20:48:52 +01:00
Result := Copy(Result, 1, Pos(';', Result) - 1);
2014-06-27 15:30:39 +02:00
end;
function TMVCRouter.IsCompatiblePath(
const AMVCPath: string;
const APath: string;
2020-09-16 15:56:14 +02:00
var aParams: TMVCRequestParamsTable): Boolean;
function ToPattern(const V: string; const Names: TList<TPair<String, String>>): string;
2013-10-30 00:48:23 +01:00
var
S: TPair<String, String>;
2013-10-30 00:48:23 +01:00
begin
Result := V;
if Names.Count > 0 then
begin
for S in Names do
begin
Result := StringReplace(
Result,
'($' + S.Key + S.Value + ')', '([' + TMVCConstants.URL_MAPPED_PARAMS_ALLOWED_CHARS + ']*)',
[rfReplaceAll]);
end;
end;
2013-10-30 00:48:23 +01:00
end;
2017-06-02 00:10:31 +02:00
var
lMatch: TMatch;
lPattern: string;
I: Integer;
lNames: TList<TPair<String, String>>;
2017-06-02 00:10:31 +02:00
lCacheItem: TMVCActionParamCacheItem;
P: TPair<string, string>;
lConv: string;
lParValue: String;
2017-06-02 00:10:31 +02:00
begin
2024-04-19 13:21:45 +02:00
if (APath = AMVCPath) or ((APath = '/') and (AMVCPath = '')) then
begin
Exit(True);
end;
2017-06-02 00:10:31 +02:00
if not FActionParamsCache.TryGetValue(AMVCPath, lCacheItem) then
begin
lNames := GetParametersNames(AMVCPath);
lPattern := ToPattern(AMVCPath, lNames);
2024-01-25 19:32:04 +01:00
lCacheItem := TMVCActionParamCacheItem.Create('^' + lPattern + '$', lNames);
FActionParamsCache.Add(AMVCPath, lCacheItem); {do not commit this!}
2017-06-02 00:10:31 +02:00
end;
2024-04-19 13:21:45 +02:00
lMatch := lCacheItem.Match(APath);
Result := lMatch.Success;
if Result then
2013-10-30 00:48:23 +01:00
begin
2024-04-19 13:21:45 +02:00
for I := 1 to Pred(lMatch.Groups.Count) do
2019-05-02 17:38:57 +02:00
begin
P := lCacheItem.Params[I - 1];
{
P.Key = Parameter name
P.Value = Converter applied to the value before to be injected (eg. :sqid)
}
lParValue := TIdURI.URLDecode(lMatch.Groups[I].Value);
if P.Value.IsEmpty then
begin
{no converter}
aParams.Add(P.Key, lParValue);
end
else
begin
lConv := P.Value;
if SameText(lConv, ':sqid') then
begin
{sqids converter (so far the only one)}
aParams.Add(P.Key, TMVCSqids.SqidToInt(lParValue).ToString);
end
else
begin
raise EMVCException.CreateFmt('Unknown converter: %s', [lConv]);
end;
end;
2019-05-02 17:38:57 +02:00
end;
2017-06-02 00:10:31 +02:00
end;
end;
function TMVCRouter.GetParametersNames(const V: string): TList<TPair<string, string>>;
2017-06-02 00:10:31 +02:00
var
S: string;
Matches: TMatchCollection;
M: TMatch;
I: Integer;
lList: TList<TPair<string, string>>;
lNameFound: Boolean;
lConverter: string;
lName: string;
2017-06-02 00:10:31 +02:00
begin
lList := TList<TPair<string, string>>.Create;
2017-06-02 00:10:31 +02:00
try
S := '\(\$([A-Za-z0-9\_]+)(\:[a-z]+)?\)';
Matches := TRegEx.Matches(V, S, [roIgnoreCase, roCompiled, roSingleLine]);
for M in Matches do
2017-06-02 00:10:31 +02:00
begin
lNameFound := False;
lConverter := '';
for I := 0 to M.Groups.Count - 1 do
2013-10-30 00:48:23 +01:00
begin
S := M.Groups[I].Value;
if Length(S) > 0 then
2013-10-30 00:48:23 +01:00
begin
if (not lNameFound) and (S.Chars[0] <> '(') and (S.Chars[0] <> ':') then
begin
lName := S;
lNameFound := True;
Continue;
end;
if lNameFound and (S.Chars[0] = ':') then
begin
lConverter := S;
end;
2013-10-30 00:48:23 +01:00
end;
end;
if lNameFound then
begin
lList.Add(TPair<string,string>.Create(lName,lConverter));
end;
2013-10-30 00:48:23 +01:00
end;
2017-06-02 00:10:31 +02:00
Result := lList;
except
lList.Free;
raise;
2013-10-30 00:48:23 +01:00
end;
end;
function TMVCRouter.GetQualifiedActionName: string;
begin
Result := Self.FControllerClazz.QualifiedClassName + '.' + Self.MethodToCall.Name;
end;
function TMVCRouter.IsHTTPAcceptCompatible(
const ARequestMethodType: TMVCHTTPMethodType;
var AAccept: string;
const AAttributes: TArray<TCustomAttribute>): Boolean;
var
I: Integer;
2014-04-01 00:02:31 +02:00
MethodAccept: string;
FoundOneAttProduces: Boolean;
2014-04-01 00:02:31 +02:00
begin
Result := False;
2020-09-16 15:56:14 +02:00
if AAccept.Contains('*/*') then // 2020-08-08
begin
Exit(True);
end;
2014-06-27 15:30:39 +02:00
FoundOneAttProduces := False;
2017-06-02 00:10:31 +02:00
for I := 0 to high(AAttributes) do
if AAttributes[I] is MVCProducesAttribute then
begin
FoundOneAttProduces := True;
MethodAccept := MVCProducesAttribute(AAttributes[I]).Value;
AAccept := GetFirstMediaType(AAccept);
2014-04-01 00:02:31 +02:00
Result := SameText(AAccept, MethodAccept, loInvariantLocale);
if Result then
Break;
end;
Result := (not FoundOneAttProduces) or (FoundOneAttProduces and Result);
2014-04-01 00:02:31 +02:00
end;
function TMVCRouter.IsHTTPContentTypeCompatible(
const ARequestMethodType: TMVCHTTPMethodType;
var AContentType: string;
const AAttributes: TArray<TCustomAttribute>): Boolean;
2014-04-01 00:02:31 +02:00
var
I: Integer;
2014-04-01 00:02:31 +02:00
MethodContentType: string;
FoundOneAttConsumes: Boolean;
begin
if ARequestMethodType in MVC_HTTP_METHODS_WITHOUT_CONTENT then
2014-10-26 20:48:52 +01:00
Exit(True);
2014-03-24 17:37:08 +01:00
Result := False;
FoundOneAttConsumes := False;
2017-06-02 00:10:31 +02:00
for I := 0 to high(AAttributes) do
if AAttributes[I] is MVCConsumesAttribute then
begin
FoundOneAttConsumes := True;
MethodContentType := MVCConsumesAttribute(AAttributes[I]).Value;
AContentType := GetFirstMediaType(AContentType);
2014-04-01 00:02:31 +02:00
Result := SameText(AContentType, MethodContentType, loInvariantLocale);
if Result then
Break;
end;
Result := (not FoundOneAttConsumes) or (FoundOneAttConsumes and Result);
end;
function TMVCRouter.IsHTTPMethodCompatible(
const AMethodType: TMVCHTTPMethodType;
const AAttributes: TArray<TCustomAttribute>): Boolean;
2013-10-30 00:48:23 +01:00
var
I: Integer;
2013-10-30 00:48:23 +01:00
MustBeCompatible: Boolean;
CompatibleMethods: TMVCHTTPMethods;
begin
Result := False;
2013-10-30 00:48:23 +01:00
MustBeCompatible := False;
2017-06-02 00:10:31 +02:00
for I := 0 to high(AAttributes) do
if AAttributes[I] is MVCHTTPMethodAttribute then
2013-10-30 00:48:23 +01:00
begin
2014-10-26 20:48:52 +01:00
MustBeCompatible := True;
CompatibleMethods := MVCHTTPMethodAttribute(AAttributes[I]).MVCHTTPMethods;
2013-10-30 00:48:23 +01:00
Result := (AMethodType in CompatibleMethods);
end;
Result := (not MustBeCompatible) or (MustBeCompatible and Result);
2013-10-30 00:48:23 +01:00
end;
2020-09-16 15:56:14 +02:00
class function TMVCRouter.StringMethodToHTTPMetod(const aValue: string): TMVCHTTPMethodType;
2013-10-30 00:48:23 +01:00
begin
2020-09-16 15:56:14 +02:00
if aValue = 'GET' then
2013-10-30 00:48:23 +01:00
Exit(httpGET);
2020-09-16 15:56:14 +02:00
if aValue = 'POST' then
2013-10-30 00:48:23 +01:00
Exit(httpPOST);
2020-09-16 15:56:14 +02:00
if aValue = 'DELETE' then
2013-10-30 00:48:23 +01:00
Exit(httpDELETE);
2020-09-16 15:56:14 +02:00
if aValue = 'PUT' then
2013-10-30 00:48:23 +01:00
Exit(httpPUT);
2020-09-16 15:56:14 +02:00
if aValue = 'HEAD' then
2013-10-30 00:48:23 +01:00
Exit(httpHEAD);
2020-09-16 15:56:14 +02:00
if aValue = 'OPTIONS' then
2013-10-30 00:48:23 +01:00
Exit(httpOPTIONS);
2020-09-16 15:56:14 +02:00
if aValue = 'PATCH' then
2014-03-24 17:37:08 +01:00
Exit(httpPATCH);
2020-09-16 15:56:14 +02:00
if aValue = 'TRACE' then
2014-03-24 17:37:08 +01:00
Exit(httpTRACE);
2020-09-16 15:56:14 +02:00
raise EMVCException.CreateFmt('Unknown HTTP method [%s]', [aValue]);
2013-10-30 00:48:23 +01:00
end;
2017-06-02 00:10:31 +02:00
{ TMVCActionParamCacheItem }
constructor TMVCActionParamCacheItem.Create(aValue: string;
aParams: TList<TPair<String, String>>);
2017-06-02 00:10:31 +02:00
begin
inherited Create;
fValue := aValue;
fParams := aParams;
fRegEx := TRegEx.Create(FValue, [roIgnoreCase, roCompiled, roSingleLine]);
2017-06-02 00:10:31 +02:00
end;
destructor TMVCActionParamCacheItem.Destroy;
begin
FParams.Free;
inherited;
end;
2024-04-19 13:21:45 +02:00
function TMVCActionParamCacheItem.Match(const Value: String): TMatch;
begin
Result := fRegEx.Match(Value);
end;
function TMVCActionParamCacheItem.Params: TList<TPair<String, String>>;
2017-06-02 00:10:31 +02:00
begin
Result := FParams;
end;
function TMVCActionParamCacheItem.Value: string;
begin
Result := FValue;
end;
2013-10-30 00:48:23 +01:00
end.