Interceptores
Visión general
Al igual que los triggers de base de datos, los interceptores son suscriptores que se ejecutan automáticamente en respuesta a operaciones de guardado (save / commit) sobre una instancia de DbContext. Pero, a diferencia de los triggers, los interceptores son de alto nivel, independientes del proveedor de base de datos, código gestionado puro y se comportan de forma parecida a los filtros de MVC.
Los interceptores permiten resolver una preocupación concreta sin tocar el núcleo de la aplicación. Crono los usa intensamente. Sirven, por ejemplo, para:
- Invalidar entradas de caché
- Actualizar datos calculados
- Validar, corregir o enriquecer una entidad antes de guardarla
- Eliminar entidades dependientes tras eliminar una entidad principal
- Registrar auditoría
- Enviar notificaciones
- Actualizar un índice
- Eliminar recursos huérfanos
Concepto
Un interceptor es un sistema pub/sub especializado sin la parte de publicación: solo puedes suscribirte a los eventos de base de datos, que se publican implícitamente durante una operación de guardado (por ejemplo SaveChanges() del CronoDbContexto).
Cada interceptor tiene un manejador PreGuardado y uno PostGuardado, que se llaman por cada entidad del Change Tracker de EF. PreGuardado se ejecuta ANTES de guardar; luego ocurre el guardado real; después se ejecuta PostGuardado.
El propósito del manejador PreGuardado es:
- Validar una entidad.
- Corregir o enriquecer una entidad.
- Cambiar el estado de una entidad (por ejemplo, para suprimir el guardado).
- Comprobar qué propiedades se han modificado (esto no es posible en PostGuardado).
El propósito del manejador PostGuardado es realizar una acción con una entidad ya guardada de forma definitiva.
Saltarse EF y acceder directamente a la base de datos con SQL crudo significa: ¡sin eventos y sin interceptores!
Implementar interceptores
Crea una clase concreta que implemente IDbGuardadoInterceptor (o IDbGuardadoInterceptor<TContexto> para atarlo a un tipo de contexto concreto), o —más cómodo— que derive de la clase base abstracta DbGuardadoInterceptorAsync<TContexto, TEntidad>, que la ata al contexto y al tipo de entidad indicados.
No hace falta registrar el interceptor en la inyección de dependencias: se detecta y registra automáticamente como servicio con ámbito al iniciar la aplicación, por lo que puede tomar cualquier dependencia.
Cuando un interceptor se ata al tipo de entidad TEntidad, coincide con todas las entidades iguales o subclases de TEntidad.
La interfaz IDbGuardadoInterceptor
public interface IDbGuardadoInterceptor
{
Task<InterceptorResultado> AlAntesDeGuardarAsync(IEntidadInterceptada entrada, CancellationToken cancelToken);
Task<InterceptorResultado> AlDespuesDeGuardarAsync(IEntidadInterceptada entrada, CancellationToken cancelToken);
Task AlAntesDeGuardarCompletadoAsync(IEnumerable<IEntidadInterceptada> entradas, CancellationToken cancelToken);
Task AlDespuesDeGuardarCompletadoAsync(IEnumerable<IEntidadInterceptada> entradas, CancellationToken cancelToken);
}
Resultado del interceptor
Cada método manejador devuelve un InterceptorResultado:
| Valor | Descripción |
|---|---|
Vacio (-1) | Señala que nunca debe procesarse el interceptor de nuevo para la combinación actual de EntidadTipo/Estado/Etapa. |
Fallido | La operación se manejó pero terminó con errores. Los interceptores fallidos quedan ausentes de los métodos ...CompletadoAsync. |
Exito | La operación se manejó y completó sin errores. |
Optimización de rendimiento
Por rendimiento, es esencial devolver InterceptorResultado.Vacio cuando la combinación actual de tipo/estado/etapa no le interesa al interceptor. Así se le indica al framework que deje de ejecutar el interceptor para esa combinación en guardados sucesivos, evitando instanciar clases una y otra vez para no hacer nada.
En lugar de devolver InterceptorResultado.Vacio también puedes lanzar NotImplementedException o NotSupportedException; se tratan igual.
Clase base abstracta
La clase base DbGuardadoInterceptorAsync<TContexto, TEntidad> ofrece seis métodos anulables (por defecto devuelven Vacio, solo hay que sobreescribir los que interesen):
- PreGuardado:
AlInsertandoAsync,AlActualizandoAsync,AlEliminandoAsync - PostGuardado:
AlInsertadoAsync,AlActualizadoAsync,AlEliminadoAsync
internal class MiInvalidadorCacheInterceptor : DbGuardadoInterceptorAsync<CronoDbContexto, Producto>
{
private readonly ICacheAdministrador _cache;
public MiInvalidadorCacheInterceptor(ICacheAdministrador cache)
{
_cache = cache;
}
protected override async Task<InterceptorResultado> AlActualizadoAsync(
Producto entidad, IEntidadInterceptada entrada, CancellationToken cancelToken)
{
// Invalidar caché relacionada tras actualizar el producto
await _cache.EliminarPorPatronAsync("producto:*");
return InterceptorResultado.Exito;
}
}
IEntidadInterceptada
Se pasa al método manejador y representa la entrada de la entidad interceptada:
| Miembro | Descripción |
|---|---|
DbContexto | El contexto de datos que activó el interceptor. |
Entrada | La EntityEntry de EF subyacente. |
Entidad | La instancia de la entidad interceptada (EntidadBase). |
EntidadTipo | El tipo real (sin proxy) de la entidad. |
Estado | El estado actual de la entidad. |
EstadoInicial | El estado antes de guardar. Úsalo en manejadores PostGuardado. |
EstadoModificado | Indica si el estado cambió durante la intercepción. |
EsPropiedadModificada(nombre) | Indica si la propiedad dada fue modificada (solo fiable en PreGuardado). |
EsEliminacionLogica | Indica si la entidad está en estado de eliminación lógica (IEliminacionLogica con Deleted = true). |
Interceptores integrados de Crono
Muchas capacidades transversales de Crono están implementadas como interceptores. Algunos ejemplos reales:
| Interceptor | Propósito |
|---|---|
AuditoriaInterceptor | Rellena las propiedades de auditoría (IAuditable) al insertar/actualizar. |
EliminacionLogicaInterceptor | Convierte las eliminaciones físicas en eliminación lógica para IEliminacionLogica. |
OrganizacionInterceptor / OrganizacionUnicaInterceptor / OrganizacionRestringidaInterceptor | Asignan y filtran la organización (multi-organización). |
PermisoInterceptor / PermisoRolInterceptor / AclRestringidoInterceptor | Aplican reglas de seguridad y ACL. |
EntidadLocalizadaInterceptor | Gestiona las entidades localizables (IEntidadLocalizada). |
MenuInterceptor / MenuElementoInterceptor | Invalidan la caché de menús al cambiar sus entidades. |
OrigenSolicitudInterceptor | Registra el canal/plataforma de origen de la solicitud. |
Consejo
La mayoría de los interceptores solo invalidan caché. Separar la invalidación del acceso a la caché puede volver el código confuso; por eso conviene combinar el interceptor y el servicio en una sola clase: la clase implementa a la vez su interfaz de servicio y hereda de DbGuardadoInterceptorAsync<TContexto, TEntidad>.