Skip to main content

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.

warning

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.

info

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:

ValorDescripción
Vacio (-1)Señala que nunca debe procesarse el interceptor de nuevo para la combinación actual de EntidadTipo/Estado/Etapa.
FallidoLa operación se manejó pero terminó con errores. Los interceptores fallidos quedan ausentes de los métodos ...CompletadoAsync.
ExitoLa 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.

info

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:

MiembroDescripción
DbContextoEl contexto de datos que activó el interceptor.
EntradaLa EntityEntry de EF subyacente.
EntidadLa instancia de la entidad interceptada (EntidadBase).
EntidadTipoEl tipo real (sin proxy) de la entidad.
EstadoEl estado actual de la entidad.
EstadoInicialEl estado antes de guardar. Úsalo en manejadores PostGuardado.
EstadoModificadoIndica si el estado cambió durante la intercepción.
EsPropiedadModificada(nombre)Indica si la propiedad dada fue modificada (solo fiable en PreGuardado).
EsEliminacionLogicaIndica 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:

InterceptorPropósito
AuditoriaInterceptorRellena las propiedades de auditoría (IAuditable) al insertar/actualizar.
EliminacionLogicaInterceptorConvierte las eliminaciones físicas en eliminación lógica para IEliminacionLogica.
OrganizacionInterceptor / OrganizacionUnicaInterceptor / OrganizacionRestringidaInterceptorAsignan y filtran la organización (multi-organización).
PermisoInterceptor / PermisoRolInterceptor / AclRestringidoInterceptorAplican reglas de seguridad y ACL.
EntidadLocalizadaInterceptorGestiona las entidades localizables (IEntidadLocalizada).
MenuInterceptor / MenuElementoInterceptorInvalidan la caché de menús al cambiar sus entidades.
OrigenSolicitudInterceptorRegistra 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>.