Daf.Web — Guía de uso
🌍 Idioma: English | Español
Tabla de contenidos
- Arquitectura
- Controladores
- Atributos de enrutamiento
- Binding de parámetros
- Referencia de action results
- La variante de aplicación Web
- TDafHttpServer
- Integración con DI
- Action results personalizados
Arquitectura
Petición HTTP
│
▼
TDafHttpServer (Indy TIdHTTPWebBrokerBridge)
│
▼
TWebModule (dispatcher WebBroker)
│
▼
TWebRouter (BeforeDispatch — casa [WebRoute] por RTTI)
│
▼
TWebController (construido para esta petición desde el scope de servicios)
│
▼
Método de acción → IActionResult → Respuesta HTTP
Controladores
TWebController
Un controlador desciende de TWebController. Es la clase que construye el router, y la que hay que usar.
uses Web.HTTPApp, Daf.Web.Controller;
type
[WebRoutePrefix('/api/usuarios')]
TUserController = class(TWebController)
private
FService: IUserService;
public
constructor Create(const Service: IUserService); reintroduce;
[WebRoute(mtGet, '/{id}')]
function GetUser(Id: Integer): IActionResult;
end;
constructor TUserController.Create(const Service: IUserService);
begin
inherited Create;
FService := Service;
end;
function TUserController.GetUser(Id: Integer): IActionResult;
var User: TUserDTO;
begin
User := FService.FindById(Id);
if User = nil then
Result := NotFound
else
Result := Ok(User);
end;TWebController extiende TDataModule, que es lo que lo hace usable desde el diseñador del IDE junto a los action items de WebBroker. Los métodos de acción deben ser public o published, o el RTTI no los verá.
TWebControllerBase
Variante de clase plana que no desciende de TDataModule. El router no la construye: construye la clase que ha casado y la trata como un TWebController, así que enrutar un descendiente de TWebControllerBase no está soportado. Úsala solo fuera del router, o no la uses.
Atributos de enrutamiento
[WebRoutePrefix]
Se aplica a la clase del controlador. Prefija todas las rutas de la clase.
[WebRoutePrefix('/api/usuarios')]
TUserController = class(TWebController)[WebRoute]
Se aplica a un método de acción. El verbo es un TMethodType, no una cadena — las constantes están en Web.HTTPApp.
[WebRoute(mtGet, '/')] // GET /api/usuarios/
[WebRoute(mtGet, '/{id}')] // GET /api/usuarios/42
[WebRoute(mtPost, '/')] // POST /api/usuarios/
[WebRoute(mtPut, '/{id}')] // PUT /api/usuarios/42
[WebRoute(mtDelete, '/{id}')] // DELETE /api/usuarios/42
[WebRoute(mtAny, '/health')] // cualquier verboLos segmentos escritos {nombre} son parámetros de ruta — se casan por nombre con los parámetros del método.
Un segmento literal se prueba antes que un parámetro en la misma posición, se declaren las acciones en el orden que se declaren: con /{id} y /me en el mismo prefijo, /api/usuarios/me llega a la acción de /me. Dos rutas que casan igual con un path —/{id} y /{nombre} en el mismo verbo— no son un error: se queda la petición la primera que el router descubrió.
Una petición cuyo path casa pero cuyo verbo no declara ninguna acción no se despacha a una acción de otro verbo: el router la deja sin atender, y el módulo web de la variante contesta 404 Not found, igual que a un path que no declara ninguna acción. Un módulo web propio pasado a WebApp.AddHttpServer contesta esas peticiones como decida.
Binding de parámetros
| Atributo | Origen | Ejemplo |
|---|---|---|
| (ninguno) | Segmento de path | [WebRoute(mtGet,'/{id}')] function Get(Id: Integer) |
[FromQuery] |
Query string | function Search([FromQuery] Q: string) |
[FromBody] |
Cuerpo de la petición (JSON) | function Create([FromBody] Cmd: TCreateUserCmd) |
[FromServices] |
Contenedor DI | function Ping([FromServices] Clock: IClock) |
Un parámetro de tipo TWebRequest o TWebResponse recibe la propia petición o respuesta, sin necesidad de atributo.
Un parámetro [FromServices] es una interfaz y se resuelve del ámbito de la petición. Un servicio que nadie registró hace fallar la petición nombrando el servicio: la acción no recibe nunca un nil.
Un parámetro [FromBody] es una clase: se construye con su constructor sin parámetros y se rellena con el cuerpo, que tiene que ser un objeto JSON. Un cuerpo vacío, que no se analiza, de otro tipo JSON o que no rellena la clase responde 400 Bad Request nombrando el parámetro, y la acción no se invoca. El objeto vive lo que la petición: el router lo libera después de ejecutar el resultado de la acción, así que una acción que tiene que guardar algo lo copia.
Un valor de ruta o de [FromQuery] se convierte al tipo de su parámetro con la cultura invariante, como en ASP.NET Core: enteros, decimales con punto sea cual sea el locale del proceso, fechas en ISO 8601 o en el formato invariante, booleanos como true o false en cualquier caja, y enumerados por nombre o por número. Un valor que no convierte responde 400 Bad Request nombrando el parámetro, y la acción no se invoca. Una clave de consulta que la petición no trae llega como el valor por defecto de su tipo: vacío, cero o falso. Un valor de ruta llega decodificado, salvo %2F, que se queda codificado y dentro de su segmento.
Referencia de action results
Todos los helpers son métodos protected del controlador. Cada helper de estado acepta un mensaje opcional, que pasa a ser el cuerpo de la respuesta — ninguno pone cabecera Location.
// 2xx
Ok(const Value: TObject): IActionResult; overload; // serializado como JSON
Ok(const Value: string): IActionResult; overload;
Ok(const Value: Boolean): IActionResult; overload;
Created(const Msg: string = 'Created'): IActionResult;
Accepted(const Msg: string = 'Accepted'): IActionResult;
NoContent(const Msg: string = 'No content'): IActionResult;
// 3xx
MovedPermanently(const Msg: string = 'Moved permanently'): IActionResult;
MovedTemporarily(const Msg: string = 'Moved temporarily'): IActionResult;
NotModified(const Msg: string = 'Not modified'): IActionResult;
// Contenido
Html(const HtmlText: string): IActionResult;
Page(const FileName: string): IActionResult; // <raíz>/pages/<nombre>.html
Partial(const FileName: string): IActionResult; // <raíz>/partials/<nombre>.html
// 4xx / 5xx
BadRequest(const Msg: string = 'Bad request'): IActionResult;
Unauthorized(const Msg: string = 'Unauthorized'): IActionResult;
Forbidden(const Msg: string = 'Forbidden'): IActionResult;
NotFound(const Msg: string = 'Not found'): IActionResult;
NotAcceptable(const Msg: string = 'Not acceptable'): IActionResult;
Conflict(const Msg: string = 'Conflict'): IActionResult;
UnprocessableEntity(const Msg: string = 'Unprocessable entity'): IActionResult;
InternalServerError(const Msg: string = 'Internal server error'): IActionResult;Una acción también puede devolver un string a secas, que se envía como text/plain.
La variante de aplicación Web
Daf.Web es una de la familia de aplicaciones, y su punto de entrada es TWebApplication. Componer el host, el servidor y el enrutado son cuatro líneas:
uses
Daf.Extensions.DependencyInjection, // IServiceCollection, AddScoped
Daf.Web.Application; // TWebApplication, WebApp
begin
var Builder := TWebApplication.CreateBuilder;
Builder.Services.AddScoped<IUserService, TUserService>;
WebApp.AddHttpServer(Builder.Services);
var App := Builder.Build;
App.MapControllers;
ExitCode := App.Run;
end.WebApp.AddHttpServerregistra el router y el servidor. Es una fachada deDaf.Weby no un miembro del componedor común, así que el paquete genérico no nombra a ninguna variante y el grafo de paquetes va en un solo sentido.MapControllersresuelve el router, y resolverlo es el descubrimiento: el router recorre la RTTI buscando[WebRoute]al construirse.Runarranca el host, arranca el servidor, espera al apagado y para los dos, y devuelveTAppExit.Oksi termina bien. Aquí el bucle es de DAF, así que el.dpracaba cuando acabaRun—al contrario que en VCL y FMX, donde el bucle es de la plataforma—.
Lo que se escapa
Builder.OnException(Policy) declara qué pasa con lo que se escapa, y con qué código de salida; sin ella, ExitPolicy.LogAndFail. Es la política de la familia de aplicaciones: los mismos TAppFailure y ExitPolicy que la variante CLI, y Daf.Web.Application los expone.
begin
var Builder := TWebApplication.CreateBuilder;
Builder.OnException(ExitPolicy.LogAndFail());
try
Builder.Services.AddScoped<IUserService, TUserService>;
WebApp.AddHttpServer(Builder.Services);
var App := Builder.Build;
App.MapControllers;
ExitCode := App.Run;
except
on E: Exception do
ExitCode := Builder.HandleException(E);
end;
end.- Lo que se escapa del arranque del host, del arranque del servidor o de la parada va a la política, y
Rundevuelve el código que ella dio al primer fallo. Un fallo del arranque llega conFailure.ReportedaTrue; el servidor no arranca, y el host se para igual. Un fallo de la parada llega conFailure.Servicesanil. - Lo que se escapa antes del
Run—la composición, elBuild— llega a la misma política cuando elexceptdel.dprse lo pasa aHandleException, que devuelve el código de salida. Antes de declararOnException, decide la política por defecto. - Un
EAbortno llega a la política:RundevuelveTAppExit.Failurey no se informa nada. - Lo que falla dentro de una petición no llega a la política: el servidor lo contesta con su respuesta de error. El gancho
ApplicationHandleExceptiondel RTL sigue siendo el que pone el manejador de peticiones de WebBroker.
El puerto
La variante siembra Web:Port con 8080 antes de entregar la configuración, así que un argumento o una variable de entorno leídos después lo pisan:
MyApp --Web:Port=9000
AddHttpServer le entrega al servidor la sección Web, y por eso la clave es Web:Port desde fuera y Port a secas dentro de TDafHttpServer.Create.
Ver La familia de aplicaciones para lo que comparten las cuatro variantes.
TDafHttpServer
TDafHttpServer = class(TInterfacedObject, IDafHttpServer)
constructor Create(const WebModuleClass: TComponentClass;
const Config: IConfiguration;
const LoggerFactory: ILoggerFactory = nil);
procedure Start;
procedure Stop;
function CanBindPort: Boolean; overload;
function CanBindPort(const APort: Integer): Boolean; overload;
property Port : Word;
property Active : Boolean; // solo lectura
property SessionID : string; // solo lectura
// Eventos: OnStarting, OnStarted, OnStopping, OnStopped, OnError, OnDestroy
end;Create lee el puerto de Config['Port']. Start se niega a arrancar sobre un puerto ocupado: dispara OnError y después lanza una excepción que nombra el puerto, antes de tocar el socket. CanBindPort responde a lo mismo por adelantado, y suelta lo que haya reservado.
OnError es un TServerErrorEvent, procedure(Sender: TObject; const Error: Exception) of object, con el servidor como Sender. La excepción sigue siendo de quien la lanza: el manejador la lee, y ni la libera ni la guarda. ASP.NET Core no tiene un evento equivalente.
Manejarlo tú
WebApp.AddHttpServer lo registra por ti, y esa es la vía. Construirlo a mano es para un proceso que no es una aplicación DAF —un banco de specs, un programa existente que adopta DAF a trozos—:
uses
Daf.Extensions.HttpServer, // IDafHttpServer
Daf.HttpServer; // TDafHttpServer
var Srv: IDafHttpServer := TDafHttpServer.Create(TMyWebModule, AppConfig, LogFactory);
Srv.Port := 8080;
Srv.Start;⚠️ La variable es una IDafHttpServer y no la clase. TDafHttpServer lleva contador de referencias, así que sostenerlo por su tipo de clase lo deja sin dueño.
⚠️ Lo que pases como AppConfig se lee con la clave Port, no Web:Port. La fachada le entrega al servidor GetSection('Web'); una configuración pasada directamente se lee en su raíz.
El servidor todavía no es un IHostedService — ver la nota del README.
Integración con DI
Cada petición tiene su propio scope, y el controlador se construye dentro de él a partir de IServiceProvider, se invoca y se libera una vez escrita la respuesta. Todo lo scoped vive y muere, por tanto, con la petición.
Las peticiones se atienden a la vez: dos pueden ejecutar la misma acción al mismo tiempo. Lo que tiene un controlador o un servicio Scoped es de su petición; un Singleton, o cualquier estado que un controlador alcance fuera de su petición, se comparte entre hilos y tiene que ser seguro para ellos.
La clase del controlador no hace falta registrarla: la construye TActivatorUtilities, que elige el constructor cuyos parámetros puede satisfacer el contenedor. Lo que sí hay que registrar es lo que el controlador pide.
Services.AddScoped<IUserService, TUserService>;Action results personalizados
Los helpers de arriba cubren los casos corrientes. Para un tipo de contenido que no producen, hereda de TActionResult — implementa IActionResult y deja Execute abstracto:
type
TCsvResult = class(TActionResult)
private
FRows: TArray<string>;
public
constructor Create(const Rows: TArray<string>);
procedure Execute(Response: TWebResponse); override;
end;
constructor TCsvResult.Create(const Rows: TArray<string>);
begin
inherited Create;
FRows := Rows;
end;
procedure TCsvResult.Execute(Response: TWebResponse);
begin
Response.StatusCode := 200;
Response.ContentType := 'text/csv; charset=utf-8';
Response.Content := string.Join(sLineBreak, FRows);
end;Úsalo en cualquier controlador:
Result := TCsvResult.Create(Rows);