FEPBA.Helper 10.9.2

dotnet add package FEPBA.Helper --version 10.9.2
                    
NuGet\Install-Package FEPBA.Helper -Version 10.9.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FEPBA.Helper" Version="10.9.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FEPBA.Helper" Version="10.9.2" />
                    
Directory.Packages.props
<PackageReference Include="FEPBA.Helper" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FEPBA.Helper --version 10.9.2
                    
#r "nuget: FEPBA.Helper, 10.9.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FEPBA.Helper@10.9.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FEPBA.Helper&version=10.9.2
                    
Install as a Cake Addin
#tool nuget:?package=FEPBA.Helper&version=10.9.2
                    
Install as a Cake Tool

FEPBA.Helper

NuGet Version License: MIT .NET

🌐 Documentation Language Selection | English

Tabla de Contenidos

Descripción General

FEPBA.Helper es una librería comprehensiva y modular para .NET 10.0 que proporciona utilidades esenciales para aplicaciones ASP.NET Core, incluyendo:

  • WebSockets y SignalR: Sistema genérico para presencia en tiempo real en pestañas/recursos, deduplicación de sesiones, chat en vivo con persistencia asíncrona por Channels, y manejo global de excepciones con SignalRExceptionFilter.
  • Mapeo Rápido de Objetos: Motor de mapeo objeto a objeto de alto rendimiento (IFepbaMapper, FepbaMapper) basado en árboles de expresiones (Expression Trees) compilados dinámicamente y extensiones fluidas (MapTo, MapToList, AddFepbaMapper).
  • Gestión de Base de Datos: Ejecución de stored procedures en SQL Server con mapeo automático, transacciones, soporte asíncrono y capacidades de registro de logs (DbManager, DbManagerContext, DbManagerContextFactory).
  • Cliente HTTP Proxy: Comunicación REST API con registro de logs integrado, manejo de encabezados predeterminados y propagación segura de credenciales (ProxyHttpClient, ProxyHttpClientFactory, IProxyHeaderBuilder).
  • Seguridad e Impersonation: Abstracción IImpersonationExecutor con soporte nativo de Windows (WindowsImpersonationExecutor) y soporte desacoplado para OAuth/JWT (PassthroughImpersonationExecutor).
  • Controladores y Servicios Base: Clases base (CustomController, CustomService) con ejecución bajo impersonation y manejo uniforme de excepciones (CustomException, ResponseApi<T>).
  • Logging y Middleware Resiliente: Registro en base de datos con recarga en caliente (IOptionsMonitor<CustomLoggerOptions>), extensión AddCustomLogger, y middleware ResponseCaptureMiddleware con bypass para WebSockets.

Esta librería está especialmente diseñada para ser utilizada por FEPBA, facilitando el desarrollo de aplicaciones web robustas, seguras y escalables.

Instalación

Prerrequisitos

  • .NET 10.0 o superior
  • Visual Studio 2022 / 2026 o VS Code
  • SQL Server (para funcionalidades de base de datos y logging)

Instalación via NuGet

Instala el paquete a través del Administrador de paquetes NuGet:

Install-Package FEPBA.Helper

O usando la CLI de .NET:

dotnet add package FEPBA.Helper

Inicio Rápido

1. Configuración en Program.cs

using FEPBA.Helper.Custom;
using FEPBA.Helper.Extensions;
using FEPBA.Helper.WebSocket.Presence;
using FEPBA.Helper.WebSocket.Chat;

var builder = WebApplication.CreateBuilder(args);

// 1. Configurar CustomLogger con recarga en caliente (appsettings.json)
builder.Services.AddCustomLogger(builder.Configuration);

// 2. Mapeo rápido de objetos
builder.Services.AddFepbaMapper();

// 3. Servicios requeridos de ASP.NET Core
builder.Services.AddHttpClient();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
builder.Services.AddProblemDetails();

// 4. WebSockets con SignalR (Presencia y Chat)
builder.Services.AddGenericPresenceSupport<MinimalUserInfo>(pingIntervalSeconds: 30, enabledByDefault: true);
builder.Services.AddGenericChatSupport<MinimalUserInfo>();

// 5. (Opcional) Impersonation para entornos OAuth / JWT / Linux
// builder.Services.AddScoped<IImpersonationExecutor, PassthroughImpersonationExecutor>();

var app = builder.Build();

// 5. Middleware (pasar prefijo de WebSockets para omitir el buffer de respuesta)
app.UseMiddleware<ResponseCaptureMiddleware>("/hubs");
app.UseExceptionHandler();

app.UseRouting();

// 6. Mapeo de Hubs de SignalR
app.MapPresenceHub<TabPresenceHub, ITabPresenceClient, MinimalUserInfo>("/hubs/tab-presence");
app.MapChatHub<LiveChatHub, ILiveChatClient, MinimalUserInfo>("/hubs/chat");

app.Run();

2. Configuración en appsettings.json

{
  "CustomLogger": {
    "ConnectionStrings": "cadena de conexión",
    "AppName": "app",
    "IsActive": true,
    "Include200": true,
    "ProcedureName": "procedure",
    "TrackMethods": [],
    "TrackService": [],
    "ForceTrackMethods": [],
    "ForceTrackService": [],
    "IgnoreMethods": [],
    "IgnoreService": []
  }
}

3. Uso Básico del Cliente HTTP

using FEPBA.Helper.WebClientProxy;

public class MiController : CustomController
{
    private readonly ProxyHttpClient _restHelper;

    public MiController(IConfiguration configuration)
    {
        _restHelper = new ProxyHttpClient(configuration);
    }

    public IActionResult GetData()
    {
        var response = _restHelper.RestCallGet("https://api.ejemplo.com/data", this);
        return response;
    }

    public IActionResult PostData(object data)
    {
        var response = _restHelper.RestCallPost("https://api.ejemplo.com/data", data, this);
        return response;
    }
}

Características

Gestión de Base de Datos

Arquitectura Modular

La librería utiliza una arquitectura basada en clases abstractas para mayor flexibilidad:

  • DbManagerBase: Clase abstracta base con toda la funcionalidad común
  • DbManager: Implementación concreta que usa CustomDbContext con connection string
  • DbManagerContext: Implementación que usa DbContextOptions para inyección de dependencias
  • CustomDbContext: DbContext personalizado con configuración de timeout
DbManager

La librería proporciona opciones avanzadas para la ejecución de stored procedures:

  • Timeout Personalizable: Modificar el tiempo límite antes de que se levante el error de Timeout al ejecutar stored procedures. El parámetro es opcional en los constructores.
  • Precisión en Nombres: Modificar el porcentaje de precisión para la comparación de los nombres de las propiedades. El parámetro es opcional en los constructores.
  • Logging de Ejecuciones: Ejecutar un stored procedure para usar un Log de todas las ejecuciones. Es necesario usar la sobrecarga del constructor que recibe como parámetros:
    • string de conexión
    • nombre del stored procedure a ejecutar
    • booleano para activar la ejecución del Log

El stored procedure de Log debe aceptar como parámetros:

  • string procedureName: nombre del stored procedure ejecutado previo
  • int ExecuteTime: tiempo de ejecución en milisegundos
using FEPBA.Helper.DB;

// Constructor básico
var dbManager = new DbManager(connectionString);

// Constructor con logging
var dbManagerWithLog = new DbManager(connectionString, logConnectionString, "LogProcedure", true);

// Constructor con timeout y precisión personalizadas
var dbManagerCustom = new DbManager(connectionString, timeoutChange: 60, precisionChange: 0.95);
Uso de DbManagerContext (Inyección de Dependencias)
using FEPBA.Helper.DB;

// Configuración en Program.cs para inyección de dependencias
builder.Services.AddDbContext<CustomDbContext>(options =>
    options.UseSqlServer(connectionString));

// Uso en un servicio con inyección de dependencias
public class MiServicio
{
    private readonly DbManagerContext _dbManager;

    public MiServicio(DbContextOptions<CustomDbContext> options)
    {
        _dbManager = new DbManagerContext(options);
    }
}
Uso de DbManagerContextFactory (DbContextFactory Pattern)
using FEPBA.Helper.DB;

// Configuración en Program.cs para DbContextFactory
builder.Services.AddDbContextFactory<CustomDbContext>(options =>
    options.UseSqlServer(connectionString));

// Uso en un servicio con DbContextFactory
public class MiServicio
{
    private readonly DbManagerContextFactory _dbManagerFactory;

    public MiServicio(IDbContextFactory<CustomDbContext> contextFactory)
    {
        _dbManagerFactory = new DbManagerContextFactory(contextFactory);
    }
}

Ventajas de DbManagerContextFactory:

  • Mejor Concurrencia: Cada operación usa su propio DbContext
  • Connection Pooling Optimizado: Liberación inmediata de conexiones
  • Ideal para Singleton Services: Perfecto para servicios de larga duración
  • Alto Throughput: Mejor rendimiento para operaciones concurrentes
DbParameter

Te permite armar la lista de parámetros para tus store procedures con código más limpio:

var parameters = new List<DbParameter>();
DbParameter.AddInputParameter(ref parameters, new Dictionary<string, object>()
{
    { "Nombre", "Juan Pérez" },
    { "Edad", 30 },
    { "FechaNacimiento", fechaNacimiento ?? DBNull.Value }
});

Cliente HTTP Proxy

Métodos Síncronos
// GET
var getResponse = _restHelper.RestCallGet(url, controller);
var (getResponse, content) = _restHelper.RestCallGet(url, controller, out content);

// GET con timeout personalizado
var getResponseWithTimeout = _restHelper.RestCallGet(url, controller, headersSend: null, timeout: 30);

// POST
var postResponse = _restHelper.RestCallPost(url, body, controller);
var (postResponse, postContent) = _restHelper.RestCallPost(url, body, controller, out postContent);

// POST con timeout personalizado
var postResponseWithTimeout = _restHelper.RestCallPost(url, body, controller, headersSend: null, timeout: 60);

// Descarga de archivos
var fileResponse = _restHelper.RestCallGetFileDownload(url, controller, "archivo.pdf", "application/pdf");

// Descarga de archivos con timeout personalizado
var fileResponseWithTimeout = _restHelper.RestCallGetFileDownload(url, controller, "archivo.pdf", "application/pdf", headersSend: null, timeout: 120);
Métodos Asíncronos
// GET asíncrono
var getResponse = await _restHelper.RestCallGetAsync(url, controller);
var (getResponse, content) = await _restHelper.RestCallGetContentAsync(url, controller);

// GET asíncrono con timeout personalizado
var getResponseWithTimeout = await _restHelper.RestCallGetAsync(url, controller, headersSend: null, timeout: 30);
var (getResponseWithTimeout, contentWithTimeout) = await _restHelper.RestCallGetContentAsync(url, controller, headersSend: null, timeout: 30);

// POST asíncrono
var postResponse = await _restHelper.RestCallPostAsync(url, body, controller);
var (postResponse, postContent) = await _restHelper.RestCallPostContentAsync(url, body, controller);

// POST asíncrono con timeout personalizado
var postResponseWithTimeout = await _restHelper.RestCallPostAsync(url, body, controller, headersSend: null, timeout: 60);
var (postResponseWithTimeout, postContentWithTimeout) = await _restHelper.RestCallPostContentAsync(url, body, controller, headersSend: null, timeout: 60);

Mapeo de Objetos (FepbaMapper y Source Generators)

La librería ofrece dos alternativas de alto rendimiento para mapeo objeto a objeto sin reflexión en tiempo de ejecución:

1. Mapeo en Tiempo de Compilación con Source Generators (Recomendado)

Utiliza el generador incremental de código Roslyn integrado en la librería para crear mapeadores con asignación directa de propiedades generadas en compilación:

using FEPBA.Helper.Mapper;

// 1. Definir una clase parcial que implemente IMapper<TSource, TDestination>
[Mapper]
public partial class UsuarioMapper : IMapper<Usuario, UsuarioDto>
{
    // El método Map(Usuario source) es generado automáticamente en tiempo de compilación.
}

Registro automático de todos los mapeadores de un ensamblado:

using FEPBA.Helper.Extensions;

// En Program.cs
builder.Services.AddFepbaMapperAssembly(typeof(Program).Assembly);

Inyección y uso en servicios:

public class UsuarioService
{
    private readonly IMapper<Usuario, UsuarioDto> _mapper;

    public UsuarioService(IMapper<Usuario, UsuarioDto> mapper)
    {
        _mapper = mapper;
    }

    public UsuarioDto ObtenerDto(Usuario usuario)
    {
        return _mapper.Map(usuario);
    }
}
2. Mapeo Dinámico con Expression Trees (FepbaMapper)

Para escenarios donde no se requiere crear clases de mapeo específicas, FepbaMapper compila dinámicamente árboles de expresiones en memoria y los cachea:

using FEPBA.Helper.Extensions;

// Mapeo directo de un objeto a un DTO
var usuarioDto = usuario.MapTo<UsuarioDto>();

// Mapeo diferido de listas o secuencias
IEnumerable<UsuarioDto> listaDtos = usuarios.MapToList<Usuario, UsuarioDto>();

Registro en Inyección de Dependencias:

using FEPBA.Helper.Extensions;
using FEPBA.Helper.Models;

// En Program.cs
builder.Services.AddFepbaMapper();

// En tu servicio o controlador
public class UsuarioService
{
    private readonly IFepbaMapper _mapper;

    public UsuarioService(IFepbaMapper mapper)
    {
        _mapper = mapper;
    }

    public UsuarioDto Obtener(Usuario usuario) => _mapper.Map<Usuario, UsuarioDto>(usuario);
}

Utilidades de Colecciones y Diccionarios

Extensiones para validación rápida y defensiva de colecciones y diccionarios:

using FEPBA.Helper.Extensions;

// Comprobar si una secuencia/colección es nula o vacía
if (usuarios.IsNullOrEmpty())
{
    // Manejar colección vacía
}

// Comprobar si contiene al menos un elemento
if (usuarios.HasValue())
{
    // Procesar elementos
}

// Comprobar si un diccionario es nulo o vacío
if (diccionario.IsNullOrEmpty())
{
    // Manejar diccionario vacío
}

Referencia de API

WebSockets y SignalR

PresenceHub<TClient, TUserData>

Clase base para Hubs de presencia en pestañas o recursos concurrentes.

public class PresenceHub<TClient, TUserData> : Hub<TClient>
    where TClient : class, IClientPresence<TUserData>
{
    protected PresenceHub(IConnectionTracker<TUserData> tracker, IOptionsMonitor<WebSocketOptions> optionsMonitor);
    protected virtual string GetGroupName(string tabOrResourceId);
    public virtual Task JoinTabSession(string tabOrResourceId, string userId, TUserData userData);
    public virtual Task LeaveTabSession(string tabOrResourceId, string userId);
    public override Task OnDisconnectedAsync(Exception? exception);
}
ChatHub<TClient, TUserData>

Clase base para Hubs de chat en vivo con encolado en memoria y persistencia en segundo plano. Hereda de PresenceHub.

public class ChatHub<TClient, TUserData> : PresenceHub<TClient, TUserData>
    where TClient : class, IChatClient<TUserData>
{
    protected ChatHub(IConnectionTracker<TUserData> tracker, IOptionsMonitor<WebSocketOptions> optionsMonitor, ChannelWriter<ChatMessage<TUserData>>? channelWriter = null);
    public virtual Task JoinChatRoom(string tabOrResourceId);
    public virtual Task SendMessageToTab(string tabOrResourceId, string userId, string messageText, TUserData senderData);
}
IConnectionTracker<TUserData> / InMemoryConnectionTracker<TUserData>

Rastreador concurrente de sesiones activas por recurso y usuario.

public interface IConnectionTracker<TUserData>
{
    bool TryAddSession(string resourceId, string userId, string connectionId, TUserData userData, HubCallerContext context);
    bool TryAddOrReplaceSession(string resourceId, string userId, string newConnectionId, TUserData userData, HubCallerContext context, out string oldConnectionId);
    bool GetOrAddSession(string resourceId, string userId, string currentConnectionId, TUserData userData, HubCallerContext context, out (string ConnectionId, TUserData UserData, HubCallerContext Context) activeSession);
    void RemoveSession(string resourceId, string userId, string connectionId);
    IEnumerable<UserPresence<TUserData>> GetActiveUsers(string resourceId);
    void KillAllConnections();
}
Extensiones de Servicio SignalR
// Presencia
public static IServiceCollection AddGenericPresenceSupport<TUserData>(this IServiceCollection services, int pingIntervalSeconds = 30, bool enabledByDefault = true);
public static HubEndpointConventionBuilder MapPresenceHub<THub, TClient, TUserData>(this IEndpointRouteBuilder endpoints, string routePattern);

// Chat
public static IServiceCollection AddGenericChatSupport<TUserData>(this IServiceCollection services);
public static IServiceCollection AddGenericChatWorker<TUserData>(this IServiceCollection services, Func<ChatMessage<TUserData>, IServiceProvider, Task> saveAction);
public static HubEndpointConventionBuilder MapChatHub<THub, TClient, TUserData>(this IEndpointRouteBuilder endpoints, string routePattern);
SignalRExceptionFilter

Filtro global (IHubFilter) para interceptar errores de métodos del Hub y conexiones, registrándolos en ICustomLogger. Ubicado en el espacio de nombres FEPBA.Helper.WebSocket.

public class SignalRExceptionFilter : IHubFilter
{
    public SignalRExceptionFilter(ICustomLogger customLogger);
    public ValueTask<object?> InvokeMethodAsync(HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object?>> next);
    public Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next);
}
WebSocketOptions
public class WebSocketOptions
{
    public int PingIntervalSeconds { get; set; } = 30;
    public bool IsEnabled { get; set; } = true;
}

Seguridad e Impersonation

IImpersonationExecutor

Estrategia para ejecutar código bajo una identidad determinada, configurable por DI.

public interface IImpersonationExecutor
{
    Task<T> RunAsync<T>(Func<Task<T>> action);
    Task RunAsync(Func<Task> action);
    T Run<T>(Func<T> action);
    void Run(Action action);
}
  • WindowsImpersonationExecutor: Ejecuta bajo WindowsIdentity.RunImpersonatedAsync del usuario autenticado en Windows (requiere IHttpContextAccessor).
  • PassthroughImpersonationExecutor: Ejecuta directamente bajo el proceso actual (para OAuth, OpenID y contenedores Linux).
CustomController

Controlador base que integra resolución de impersonation y captura uniforme de errores.

public abstract class CustomController : Controller
{
    public ObjectResult CustomErrorStatusCode(Exception e);
    public void ImpersontedControllerAction(Action serviceMethod);
    public Tout ImpersontedControllerAction<Tout>(Func<Tout> serviceMethod);
    public Task ImpersontedControllerActionAsync(Func<Task> serviceMethod);
    public Task<Tout> ImpersontedControllerActionAsync<Tout>(Func<Task<Tout>> serviceMethod);
}

Logging y Configuración

CustomLoggerServiceCollectionExtensions

Método de extensión ubicado en el espacio de nombres FEPBA.Helper.Extensions:

public static IServiceCollection AddCustomLogger(this IServiceCollection services, IConfiguration configuration, string sectionName = "CustomLogger");
CustomLogger
public class CustomLogger : ICustomLogger
{
    public CustomLogger(ILogger<CustomLogger> logger, IOptionsMonitor<CustomLoggerOptions> optionsMonitor);
    public Task<Response> LogAsync(HttpContext context, string pathBackend, string requestBody, int statusCode, Exception exception, string responseBody);
}
ResponseCaptureMiddleware
public class ResponseCaptureMiddleware
{
    public ResponseCaptureMiddleware(RequestDelegate next, string? bypassPaths = null);
    public Task InvokeAsync(HttpContext httpContext, ICustomLogger customLogger);
}

Cliente HTTP Proxy

ProxyHttpClient y ProxyHttpClientFactory
public ProxyHttpClient(IConfiguration config, string url, Dictionary<string, string> headers = null);
public ProxyHttpClient(IConfiguration config, Dictionary<string, string> headers = null);

public ProxyHttpClientFactory(IConfiguration config, string url, IHttpClientFactory httpClientFactory, Dictionary<string, string> headers = null);
public ProxyHttpClientFactory(IConfiguration config, IHttpClientFactory httpClientFactory, Dictionary<string, string> headers = null);

Métodos Principales:

IActionResult RestCallGet(string uri, Controller api, Dictionary<string, string> headersSend = null, int? timeout = null);
IActionResult RestCallPost(string uri, object body, Controller api, Dictionary<string, string> headersSend = null, int? timeout = null);
Task<IActionResult> RestCallGetAsync(string url, Controller api, Dictionary<string, string> headersSend = null, int? timeout = null);
Task<IActionResult> RestCallPostAsync(string url, object body, Controller api, Dictionary<string, string> headersSend = null, int? timeout = null);
Task<(IActionResult, string)> RestCallGetContentAsync(string url, Controller api, Dictionary<string, string> headersSend = null, int? timeout = null);
Task<(IActionResult, string)> RestCallPostContentAsync(string url, object body, Controller api, Dictionary<string, string> headersSend = null, int? timeout = null);
IActionResult RestCallGetFileDownload(string url, Controller api, string fileName = "HelperDownload", string fileType = "application/json", Dictionary<string, string> headersSend = null, int? timeout = null);
Task<IActionResult> RestCallGetFileDownloadAsync(string url, Controller api, string fileName = "HelperDownload", string fileType = "application/json", Dictionary<string, string> headersSend = null, int? timeout = null);
IProxyHeaderBuilder / ProxyHeaderBuilder
public interface IProxyHeaderBuilder
{
    Dictionary<string, string> BuildForwardHeaders();
}

Acceso a Base de Datos (DbManager)

Constructores y Modos
// Conexión directa
public DbManager(string connectionString, int? timeoutChange = null, double? precisionChange = null);
public DbManager(string connectionString, string logString, string logProcedure, bool? flagLog = null, int? timeoutChange = null, double? precisionChange = null);

// Inyección de dependencias
public DbManagerContext(DbContextOptions<CustomDbContext> options, int? timeoutChange = null, double? precisionChange = null);

// Factoría para servicios Singleton
public DbManagerContextFactory(IDbContextFactory<CustomDbContext> contextFactory, int? timeoutChange = null, double? precisionChange = null);

Modelos de Respuesta

ResponseApi<T>
public class ResponseApi<T> where T : class
{
    public HttpStatusCode ok { get; set; }
    public string message { get; set; }
    public T data { get; set; }
    public string developerMessage { get; set; }
    public int errorCode { get; set; }
    public string exception { get; set; }
}

Mapeo de Objetos

Source Generators y Mapeadores Fuertemente Tipados (FEPBA.Helper.Mapper)
public interface IMapper<in TSource, out TDestination>
{
    TDestination Map(TSource source);
}

public abstract class BaseMapper<TSource, TDestination>
{
    public abstract TDestination Map(TSource source);
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class MapperAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class MapMethodAttribute : Attribute { }
FepbaMapper Dinámico (FEPBA.Helper.Models)
public interface IFepbaMapper
{
    TDestination Map<TSource, TDestination>(TSource source);
}

public class FepbaMapper : IFepbaMapper
{
    public TDestination Map<TSource, TDestination>(TSource source);
}
Métodos de Extensión de Mapeo (FEPBA.Helper.Extensions)
public static class MapperExtensions
{
    public static TDestination MapTo<TDestination>(this object source) where TDestination : new();
    public static IEnumerable<TDestination> MapToList<TSource, TDestination>(this IEnumerable<TSource> source) where TDestination : new();
}

public static class MapperServiceCollectionExtensions
{
    public static IServiceCollection AddFepbaMapper(this IServiceCollection services);
    public static IServiceCollection AddFepbaMapperAssembly(this IServiceCollection services, Assembly assembly);
}

Utilidades de Colecciones y Diccionarios

CollectionExtensions
public static class CollectionExtensions
{
    public static bool IsNullOrEmpty<T>(this IEnumerable<T> collection);
    public static bool HasValue<T>(this IEnumerable<T> collection);
}
DictionaryExtensions
public static class DictionaryExtensions
{
    public static bool IsNullOrEmpty<TKey, TValue>(this IDictionary<TKey, TValue> dictionary);
}

Configuración

Configuración Tipada con CustomLoggerOptions

La forma recomendada de usar la configuración es mediante la clase CustomLoggerOptions, que proporciona acceso fuertemente tipado a la configuración del logger.

Nota: CustomLogger utiliza IOptionsMonitor<CustomLoggerOptions> para recarga en caliente de configuración en tiempo de ejecución sin necesidad de reiniciar la aplicación, y registra peticiones y respuestas de forma asíncrona mediante LogAsync(...).

Opción 1: Usando AddCustomLogger (Recomendado)

En Program.cs:

using FEPBA.Helper.Custom;

var builder = WebApplication.CreateBuilder(args);

// Registra CustomLoggerOptions y ICustomLogger en una sola línea
builder.Services.AddCustomLogger(builder.Configuration);

// Servicios requeridos
builder.Services.AddHttpClient();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
builder.Services.AddProblemDetails();

var app = builder.Build();
app.Run();
Opción 2: Usando Configure<CustomLoggerOptions> Manualmente

En Program.cs:

using FEPBA.Helper.Custom;
using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);

// Configurar la sección CustomLogger como opciones tipadas
builder.Services.Configure<CustomLoggerOptions>(
    builder.Configuration.GetSection("CustomLogger"));

// Registrar servicios
builder.Services.AddHttpClient();
builder.Services.AddScoped<ICustomLogger, CustomLogger>();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
builder.Services.AddProblemDetails();

var app = builder.Build();
app.Run();

En un servicio:

using FEPBA.Helper.Custom;
using Microsoft.Extensions.Options;

public class MiServicio
{
    private readonly CustomLoggerOptions _loggerOptions;

    public MiServicio(IOptions<CustomLoggerOptions> loggerOptions)
    {
        _loggerOptions = loggerOptions.Value;
    }

    public void EjecutarOperacion()
    {
        if (_loggerOptions.IsActive)
        {
            Console.WriteLine($"Logging activo: {_loggerOptions.ProcedureName}");
        }
    }
}

Comparación rápida:

  • Configure<CustomLoggerOptions>(...) es directo y suficiente para la mayoría de los casos.
  • AddOptions<CustomLoggerOptions>().BindConfiguration(...) es preferible cuando quieres añadir validaciones (ValidateDataAnnotations, ValidateOnStart) o validación personalizada.
Opción 2: Usando IOptionsSnapshot<T> (Para cambios sin reinicio)
public class MiServicio
{
    private readonly IOptionsSnapshot<CustomLoggerOptions> _loggerOptions;

    public MiServicio(IOptionsSnapshot<CustomLoggerOptions> loggerOptions)
    {
        _loggerOptions = loggerOptions;
    }

    public void EjecutarOperacion()
    {
        // Siempre obtiene la configuración más reciente
        if (_loggerOptions.Value.IsActive)
        {
            // Hacer algo
        }
    }
}
Opción 3: Usando IOptionsMonitor<T> (Para monitoreo en tiempo real)
public class MiServicio
{
    private readonly IOptionsMonitor<CustomLoggerOptions> _loggerOptions;

    public MiServicio(IOptionsMonitor<CustomLoggerOptions> loggerOptions)
    {
        _loggerOptions = loggerOptions;
        
        // Suscribirse a cambios
        loggerOptions.OnChange((options, name) =>
        {
            Console.WriteLine("Configuración de logger actualizada");
        });
    }

    public void EjecutarOperacion()
    {
        if (_loggerOptions.CurrentValue.IsActive)
        {
            // Hacer algo
        }
    }
}

#### Opción 4: Usando AddOptions<T> con Validaciones (Recomendado para Producción)

Esta opción es más robusta porque valida la configuración al iniciar la aplicación, lo que ayuda a detectar errores temprano.

**En Program.cs:**
```csharp
using FEPBA.Helper.Custom;
using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOptions<CustomLoggerOptions>()
    .BindConfiguration("CustomLogger")
    .ValidateDataAnnotations()
    .ValidateOnStart();

builder.Services.AddHttpClient();
builder.Services.AddScoped<ICustomLogger, CustomLogger>();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
builder.Services.AddProblemDetails();

var app = builder.Build();
app.UseExceptionHandler();
app.Run();

CustomLoggerOptions con validaciones:

using System.ComponentModel.DataAnnotations;

public class CustomLoggerOptions
{
    [Required(ErrorMessage = "ConnectionStrings es requerida")]
    public string ConnectionStrings { get; set; }
    public string AppName { get; set; }

    [Required(ErrorMessage = "ProcedureName es requerida")]
    public string ProcedureName { get; set; }

    public bool IsActive { get; set; } = false;
    public bool Include200 { get; set; } = false;
    public List<string> TrackMethods { get; set; } = new();
    public List<string> TrackService { get; set; } = new();
    public List<string> ForceTrackMethods { get; set; } = new();
    public List<string> ForceTrackService { get; set; } = new();
    public List<string> IgnoreMethods { get; set; } = new();
    public List<string> IgnoreService { get; set; } = new();
}

Validación personalizada opcional:

builder.Services.AddOptions<CustomLoggerOptions>()
    .BindConfiguration("CustomLogger")
    .ValidateDataAnnotations()
    .Validate(options =>
    {
        if (options.IsActive && string.IsNullOrEmpty(options.ConnectionStrings))
        {
            return false;
        }
        return true;
    }, "ConnectionStrings no puede estar vacía si IsActive es true")
    .ValidateOnStart();

En un servicio:

using FEPBA.Helper.Custom;
using Microsoft.Extensions.Options;

public class MiServicio
{
    private readonly CustomLoggerOptions _loggerOptions;

    public MiServicio(IOptions<CustomLoggerOptions> loggerOptions)
    {
        _loggerOptions = loggerOptions.Value;
    }

    public void EjecutarOperacion()
    {
        if (_loggerOptions.IsActive)
        {
            Console.WriteLine($"Logger activo con procedure: {_loggerOptions.ProcedureName}");
        }
    }
}

Estructura de appsettings.json

CustomLogger
{
  "CustomLogger": {
    "ConnectionStrings": "Server=localhost;Database=Logs;Trusted_Connection=true;TrustServerCertificate=true",
    "AppName": "app",
    "IsActive": true,
    "Include200": true,
    "ProcedureName": "AddRequestResposeLog",
    "TrackMethods": [
      "GetUsuarios",
      "CrearUsuario"
    ],
    "TrackService": [
      "UsuarioService",
      "ProductoService"
    ],
    "ForceTrackMethods": [
      "Login",
      "DeleteUsuario"
    ],
    "ForceTrackService": [
      "AuthService"
    ],
    "IgnoreMethods": [
      "HealthCheck",
      "GetVersion"
    ],
    "IgnoreService": [
      "HealthCheckService"
    ]
  }
}

Descripción de propiedades:

  • ConnectionStrings: Cadena de conexión a la base de datos para logging
  • AppName: Nombre de la aplicacion para el logging
  • IsActive: Habilita/deshabilita el logging
  • Include200: Registra respuestas exitosas (código 200)
  • ProcedureName: Nombre del stored procedure para guardar logs
  • TrackMethods: Métodos a registrar (si está vacío, registra todos)
  • TrackService: Servicios a registrar (si está vacío, registra todos)
  • ForceTrackMethods: Métodos que siempre se registran (incluso si están en IgnoreMethods)
  • ForceTrackService: Servicios que siempre se registran (incluso si están en IgnoreService)
  • IgnoreMethods: Métodos a excluir del registro
  • IgnoreService: Servicios a excluir del registro
ErrorHandling (Opcional)

Opcionalmente se puede agregar en el archivo appsettings.json una sección llamada "ErrorHandling" para guardar mensajes predefinidos para los códigos de error HTTP:

{
  "ErrorHandling": {
    "404": "Recurso no encontrado",
    "418": "Soy una tetera",
    "500": "Error interno del servidor"
  }
}

Store Procedure para Logging

El Proxy utiliza un store procedure con estos campos:

CREATE PROCEDURE [dbo].[AddRequestResposeLog] 
    @Username [varchar](100) NULL,
    @RequestMethod [varchar](100) NULL,
    @UrlRequestFrontend [varchar](100) NULL,
    @UrlRequestBackend [varchar](100) = NULL,
    @QueryString [varchar](100) = NULL,
    @RequestHeaders [nvarchar](max) NULL,
    @RequestBody [nvarchar](max) = NULL,
    @FrontendException [nvarchar](max) = NULL,
    @BackendResponse [nvarchar](max) = NULL,
    @StatusCode [int] NULL,
    @new_identity INT = NULL OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    
    INSERT INTO [dbo].[Logger]
           ([Username], [RequestMethod], [UrlRequestFrontend], [UrlRequestBackend], 
            [QueryString], [RequestHeaders], [RequestBody], [FrontendException], 
            [BackendResponse], [StatusCode])
    VALUES
           (@Username, @RequestMethod, @UrlRequestFrontend, @UrlRequestBackend, 
            @QueryString, @RequestHeaders, @RequestBody, @FrontendException, 
            @BackendResponse, @StatusCode);

    SET @new_identity = SCOPE_IDENTITY();
    SELECT @new_identity as N'New_Identity'
END

WebSockets con SignalR (Presencia y Chat)

La librería incluye un subsistema completo sobre SignalR para manejar presencia distribuida por recurso/pestaña, deduplicación de sesiones y salas de chat con persistencia asíncrona desacoplada mediante System.Threading.Channels.

1. Definición en la Aplicación
using FEPBA.Helper.WebSocket;
using FEPBA.Helper.WebSocket.Presence;
using FEPBA.Helper.WebSocket.Chat;
using Microsoft.Extensions.Options;
using System.Threading.Channels;

// Modelo con los datos mínimos del usuario (avatar, nombre, rol, etc.)
public record MinimalUserInfo(string DisplayName, string AvatarUrl);

// Interfaz fuertemente tipada para el cliente de Presencia
public interface ITabPresenceClient : IClientPresence<MinimalUserInfo> { }

// Hub concreto de Presencia que hereda de la librería
public class TabPresenceHub : PresenceHub<ITabPresenceClient, MinimalUserInfo>
{
    public TabPresenceHub(
        IConnectionTracker<MinimalUserInfo> tracker,
        IOptionsMonitor<WebSocketOptions> optionsMonitor)
        : base(tracker, optionsMonitor)
    {
    }
}

// Interfaz fuertemente tipada para el cliente de Chat
public interface ILiveChatClient : IChatClient<MinimalUserInfo> { }

// Hub concreto de Chat que hereda de la librería
public class LiveChatHub : ChatHub<ILiveChatClient, MinimalUserInfo>
{
    public LiveChatHub(
        IConnectionTracker<MinimalUserInfo> tracker,
        IOptionsMonitor<WebSocketOptions> optionsMonitor,
        Channel<ChatMessage<MinimalUserInfo>> channel)
        : base(tracker, optionsMonitor, channel.Writer)
    {
    }
}
2. Configuración en Program.cs
using FEPBA.Helper.WebSocket.Presence;
using FEPBA.Helper.WebSocket.Chat;

var builder = WebApplication.CreateBuilder(args);

// Registrar soporte de Presencia (KeepAlive configurable y switch en caliente)
builder.Services.AddGenericPresenceSupport<MinimalUserInfo>(pingIntervalSeconds: 30, enabledByDefault: true);

// Registrar soporte de Chat en RAM mediante Channels
builder.Services.AddGenericChatSupport<MinimalUserInfo>();

// Registrar el Worker de persistencia en segundo plano
builder.Services.AddGenericChatWorker<MinimalUserInfo>(async (message, serviceProvider) =>
{
    // Resolver el DbContext o Repositorio scoped de la aplicación
    var dbContext = serviceProvider.GetRequiredService<MiDbContext>();

    var entity = new MensajeChatEntity
    {
        UsuarioId = message.SenderId,
        Texto = message.MessageText,
        FechaCreacion = message.Timestamp,
        NombreEmisor = message.SenderData.DisplayName
    };

    dbContext.MensajesChat.Add(entity);
    await dbContext.SaveChangesAsync();
});

var app = builder.Build();

// NOTA IMPORTANTE: Excluir las rutas de SignalR/WebSockets del buffer del ResponseCaptureMiddleware
app.UseMiddleware<ResponseCaptureMiddleware>("/hubs");

app.UseRouting();

// Mapear los Hubs
app.MapPresenceHub<TabPresenceHub, ITabPresenceClient, MinimalUserInfo>("/hubs/tab-presence");
app.MapChatHub<LiveChatHub, ILiveChatClient, MinimalUserInfo>("/hubs/chat");

app.Run();

Impersonation y Autenticación (IImpersonationExecutor)

La arquitectura de CustomController y CustomService desacopla la suplantación de identidad mediante la interfaz IImpersonationExecutor:

  • WindowsImpersonationExecutor: Ejecuta la acción bajo el token de WindowsIdentity del usuario actual de Windows (ideal para IIS y Windows Auth). Si no hay un WindowsIdentity válido, se ejecuta sin fallar (null-safe).
  • PassthroughImpersonationExecutor: Ejecuta la acción directamente bajo el proceso actual sin suplantar a nivel de sistema operativo (ideal para contenedores Linux, Kubernetes y arquitecturas basadas en OAuth / OpenID / JWT).
Registro en Program.cs:
using FEPBA.Helper.Custom;

// Para entornos OAuth / Linux / Docker:
builder.Services.AddScoped<IImpersonationExecutor, PassthroughImpersonationExecutor>();

// Si no se registra ningún executor, CustomController utiliza WindowsImpersonationExecutor
// por defecto para mantener 100% de compatibilidad hacia atrás.
Uso en Controladores:
public class FacturacionController : CustomController
{
    [HttpGet]
    public IActionResult Generar()
    {
        return ImpersontedControllerAction(() =>
        {
            // Código ejecutado bajo la identidad resuelta por IImpersonationExecutor
            return Ok("Factura generada");
        });
    }

    [HttpPost]
    public async Task<IActionResult> ProcesarAsync([FromBody] Datos datos)
    {
        return await ImpersontedControllerActionAsync(async () =>
        {
            // Operación asíncrona bajo la identidad resuelta
            return Ok(await Servicio.ProcesarAsync(datos));
        });
    }
}

Propagación de Encabezados y Tokens (IProxyHeaderBuilder)

Cuando una API actúa como proxy o gateway hacia microservicios internos, ProxyHeaderBuilder copia los encabezados del request entrante (omitiendo encabezados de transporte que romperían la conexión) y convierte automáticamente tokens de autenticación:

using FEPBA.Helper.WebClientProxy;

builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<IProxyHeaderBuilder, ProxyHeaderBuilder>();

Si el JWT actual contiene el claim basic_auth_credentials, el encabezado Authorization: Bearer <token> entrante se transforma automáticamente en Authorization: Basic <credentials> para el microservicio backend.

Migration Guide

From Previous Architecture

Si estabas usando una versión anterior de FEPBA.Helper, así es cómo migrar a la nueva arquitectura modular:

Código Antiguo (Versión Anterior)
// Anteriormente podías tener algo como esto
var context = new DbManagerContext(connectionString);
Código Nuevo (Versión Actual)
//  Usar DbManagerContext con DbContextOptions (para inyección de dependencias)
var dbManager = new DbManagerContext(options);
Beneficios de la Migración
  1. Mejor Separación de Responsabilidades: DbManagerBase maneja toda la lógica común
  2. Arquitectura Flexible: Elegir entre connection string o inyección de dependencias
  3. Mejor Capacidad de Prueba: Más fácil de simular y hacer pruebas unitarias
Cambios Ruptores
  • DbManagerContext ahora extiende DbManagerBase en lugar de implementación directa
  • Las firmas de los constructores han sido actualizadas para una mayor flexibilidad

Ejemplos

Ejemplo Completo: Controlador de API

using Microsoft.AspNetCore.Mvc;
using FEPBA.Helper.Custom;
using FEPBA.Helper.WebClientProxy;
using FEPBA.Helper.Models;

[ApiController]
[Route("api/[controller]")]
public class UsuariosController : CustomController
{
    private readonly ProxyHttpClient _restHelper;
    private readonly DbManager _dbManager;

    public UsuariosController(IConfiguration configuration)
    {
        _restHelper = new ProxyHttpClient(configuration);
        _dbManager = new DbManager(configuration.GetConnectionString("DefaultConnection"));
    }

    [HttpGet]
    public async Task<IActionResult> GetUsuarios()
    {
        try
        {
            var response = await _restHelper.RestCallGetAsync("https://api.externa.com/usuarios", this);
            return response;
        }
        catch (Exception ex)
        {
            return CustomErrorStatusCode(new CustomException("Error al obtener usuarios", 1001, ex));
        }
    }

    [HttpPost]
    public IActionResult CrearUsuario([FromBody] Usuario usuario)
    {
        try
        {
            var parameters = new List<DbParameter>();
            DbParameter.AddInputParameter(ref parameters, new Dictionary<string, object>()
            {
                { "Nombre", usuario.Nombre },
                { "Email", usuario.Email },
                { "FechaCreacion", DateTime.Now }
            });

            var result = _dbManager.ExecuteProcedure("CrearUsuario", parameters);
            
            return Ok(new ResponseApi<object>(HttpStatusCode.OK, "Usuario creado exitosamente", result));
        }
        catch (Exception ex)
        {
            return CustomErrorStatusCode(new CustomException("Error al crear usuario", 1002, ex));
        }
    }
}

Ejemplos de Uso del Cliente HTTP

// Ejemplo de una petición GET
var logGetResponse = restHelper.RestCallGet(urilogGet, this);

// Ejemplo de una petición POST
// En este ejemplo se hace una petición POST para enviar un email
Mail mail = new Mail()
{
    To = "mail@ejemplo.com",
    Subject = "Rellena con tu subject",
    Body = bodyHtml,
    HTML = true,
    From = "no-responder@fepba.gov.ar"
};
var PostResponse = restHelperEnviar.RestCallPost(uri, mail, this);

Formato de Respuestas

Maneja las respuestas de las peticiones y los errores según tus necesidades. Nuestras respuestas suelen ser JSON con el siguiente formato:

{
  "ok": "STATUS CODE",
  "data": "DATA DE LA RESPUESTA",
  "message": "MENSAJE DE LA RESPUESTA"
};

Ejemplo: Manejo de Errores Personalizado

using FEPBA.Helper.Custom;

public class MiServicio
{
    public void ValidarDatos(Datos datos)
    {
        if (datos == null)
            throw new CustomException("Los datos son requeridos", 2001);
        
        if (string.IsNullOrEmpty(datos.Nombre))
            throw new CustomException("El nombre es requerido", 2002);
        
        if (datos.Edad < 18)
            throw new CustomException("El usuario debe ser mayor de edad", 2003);
    }
}

Troubleshooting

Problemas Comunes

1. Timeout en Stored Procedures

Problema: Las operaciones de base de datos exceden el tiempo límite.

Solución:

var dbManager = new DbManager(connectionString, timeoutChange: 120); // 2 minutos
2. Errores de Conexión HTTP

Problema: Fallos en llamadas a APIs externas.

Solución:

// Agregar headers personalizados
var headers = new Dictionary<string, string>
{
    { "Authorization", "Bearer token" },
    { "User-Agent", "MiApp/1.0" }
};
var response = _restHelper.RestCallGet(url, this, headers);
3. Problemas de Logging

Problema: No se registran las llamadas en la base de datos.

Solución:

  • Verificar que IsActive esté en true
  • Comprobar la cadena de conexión
  • Validar que el stored procedure exista
4. Errores de Serialización JSON

Problema: Los objetos no se serializan correctamente.

Solución:

// Usar atributos de Newtonsoft.Json
[JsonObject(MemberSerialization.OptIn)]
public class MiObjeto
{
    [JsonProperty("nombre")]
    public string Nombre { get; set; }
}
5. Fallo de Conexión en SignalR (Middleware Buffer)

Problema: Al conectar clientes a un Hub de SignalR, la negociación inicial o WebSocket falla o se bloquea.

Solución: El middleware ResponseCaptureMiddleware captura y almacena en buffer el cuerpo de respuesta para logging. Los WebSockets requieren omitir este buffer. Configura el middleware especificando el prefijo de tus Hubs:

app.UseMiddleware<ResponseCaptureMiddleware>("/hubs");
6. Error de Plataforma con Impersonation en Linux o Docker

Problema: Excepción de plataforma o fallo de autenticación al ejecutar CustomController o CustomService fuera de Windows / IIS.

Solución: Registrar PassthroughImpersonationExecutor en Program.cs para entornos basados en tokens (JWT/OAuth):

builder.Services.AddScoped<IImpersonationExecutor, PassthroughImpersonationExecutor>();
7. Desconexión en Caliente de WebSockets

Problema: Necesidad de apagar inmediatamente todos los sockets y rechazar nuevas conexiones sin reiniciar la aplicación.

Solución: Actualizar appsettings.json en tiempo de ejecución:

{
  "WebSocketOptions": {
    "IsEnabled": false
  }
}

El MonitorService detectará el cambio y ejecutará KillAllConnections() de inmediato.

Preguntas Frecuentes

Q: ¿Cómo deshabilitar el logging? A: Establecer IsActive: false en la configuración del logger.

Q: ¿Cómo manejar respuestas que no son JSON? A: Usar los métodos que devuelven string para obtener el contenido crudo.

Q: ¿Es compatible con .NET 8? A: La clase HttpHelperRestConections será eliminada en .NET 8+, pero ProxyHttpClient es compatible.

Style Guidelines

  • Seguir las convenciones de C# de Microsoft
  • Agregar pruebas unitarias para nuevas funcionalidades
  • Documentar código público con XML comments
  • Mantener compatibilidad con .NET 10.0

Local Development

# Restaurar paquetes
dotnet restore

# Compilar solución
dotnet build

# Ejecutar pruebas
dotnet test

License

Este proyecto está licenciado bajo la Licencia MIT. Ver el archivo LICENSE para más detalles.

Credits

Autores: Mauricio Agosta, Lautaro Petrigh

Equipo de Informática de FEPBA


Nota Importante: La clase HttpHelperRestConections será eliminada en versiones futuras (.NET 8+). Se recomienda migrar a ProxyHttpClient.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.9.2 0 10/21/2026
10.9.1 43 10/20/2026
10.9.0 39 10/20/2026
10.8.5 51 10/15/2026
10.8.4 70 10/14/2026
10.8.1 66 10/14/2026
10.8.0 63 10/14/2026
10.4.6 170 7/2/2026
10.3.2 133 5/20/2026
10.3.0 152 5/8/2026
10.2.0 153 3/12/2026
10.1.3 140 3/11/2026
10.1.3-alpha 121 2/13/2026
10.1.2 164 2/10/2026
8.13.2 156 2/10/2026
8.12.1 227 2/5/2026
Loading failed

Nuevo mapper y extenciones de mapeo, todas las extenciones se movieron a la carpeta Extensions