No solo appsettings.json — gestión práctica de configuración en aplicaciones de negocio Windows (configuración por entorno, secretos y ubicaciones de escritura)
· Actualizado el: · Go Komura · CSharp, .NET, appsettings.json, IConfiguration, IOptions, Generic Host, Gestión de configuración, Desarrollo de Windows, Consultoría técnica
Historial de revisiones (1 actualizaciones, última el 22 Aug 2026)
Registro de los cambios realizados en este artículo. Cuando se archivó una versión previa, sigue siendo legible mediante un enlace permanente con DOI.
- Se ha sustituido la traducción, que estaba abreviada, por una traducción completa del artículo japonés en su versión actual: el texto crece un 57 %. Se han incorporado 7 filas de tabla y 1 bloque de código que no estaban en la edición anterior. El contenido no cambia respecto al original japonés; esta edición simplemente ya no lo resume. Además, los enlaces a otros artículos que ya tienen edición en español apuntan ahora a esa edición en lugar de a la japonesa. Leer la versión anterior a esta actualización (DOI: 10.5281/zenodo.21638295)
- Primera publicación
Citar este artículo(DOI: 10.5281/zenodo.21638294)
Este artículo está archivado en Zenodo. A continuación se muestran tanto el DOI que siempre resuelve a la última versión como el DOI fijado a la versión que está leyendo.
Go Komura (2026). No solo appsettings.json — gestión práctica de configuración en aplicaciones de negocio Windows (configuración por entorno, secretos y ubicaciones de escritura). KomuraSoft LLC. https://doi.org/10.5281/zenodo.21638294 https://comcomponent.com/es/blog/dotnet-configuration-management-guide/
- DOI (última versión)
- 10.5281/zenodo.21638294
- DOI (esta versión)
- 10.5281/zenodo.22053444
«Quiero cambiar la cadena de conexión según el entorno», «quiero guardar los ajustes de visualización por usuario», «he terminado escribiendo la clave de API directamente en appsettings.json». En la gestión de configuración de las aplicaciones de negocio Windows, cualquiera llega hasta el punto de colocar un único appsettings.json y leerlo desde IConfiguration, pero lo que viene después —la configuración por entorno, la ubicación de los ajustes escribibles, el tratamiento de los secretos, los cambios de configuración durante la ejecución— es un terreno que, sorprendentemente, a menudo entra en producción sin haberse organizado.
Este artículo organiza, en el orden en que suelen surgir las dudas en la práctica, desde IConfiguration y la superposición de proveedores —el fundamento del sistema de configuración de .NET—, pasando por la configuración por entorno, la forma de recibir valores con seguridad de tipos mediante el patrón IOptions, la ubicación de los ajustes escribibles, el tratamiento de los secretos y los límites reales de los cambios de configuración durante la ejecución, hasta el mapa de migración desde app.config/Settings.settings.
Como el artículo es largo, con 8 capítulos en total, mostramos antes que nada una guía de lectura según el objetivo.
| Situación del lector | Orden de lectura recomendado |
|---|---|
| Va a diseñar la gestión de configuración desde cero | Capítulo 1 (tabla de decisión) → capítulos 2 a 4 (fundamentos, configuración por entorno, patrón de opciones). Los capítulos 5 y 6 puede leerlos cuando le surja la necesidad de escribir ajustes o de tratar secretos |
Va a migrar desde el app.config de una aplicación existente |
Capítulo 8 (mapa de migración) → capítulo 1 (tabla de decisión) → capítulo 2. Es más rápido entender primero el destino de la migración y luego ir resolviendo cada elemento con la tabla de correspondencias |
| Está investigando por qué «cambié la configuración pero no se refleja» | Capítulo 2 (prioridad de los proveedores) y capítulo 7 (límites de reloadOnChange). La mayoría de las causas están en uno de estos dos capítulos |
| Solo quiere saber dónde guardar los secretos | Capítulo 6. Tenga en cuenta primero que el mecanismo es distinto en desarrollo y en producción |
| Solo quiere saber dónde guardar la configuración (por usuario o por máquina) | Capítulo 5 |
1. Ante todo, la conclusión
Las decisiones de gestión de configuración empiezan por determinar la «ubicación» según el «tipo de ajuste». Primero resumimos la visión general en una tabla de decisión.
| Tipo de ajuste | Ejemplos concretos | Primera opción de ubicación | Motivo |
|---|---|---|---|
| Valores predeterminados de la aplicación | Nivel de registro predeterminado, parámetros predeterminados de la interfaz | appsettings.json |
Valores comunes independientes del entorno que se incluyen en el artefacto de compilación |
| Configuración por entorno | Cadena de conexión o extremo de API que cambia entre el entorno de pruebas y el de producción | appsettings.{Environment}.json + variables de entorno |
Se puede aprovechar tal cual el orden de sobrescritura predeterminado |
| Configuración por usuario | Última carpeta abierta, posición de la ventana, ajustes de visualización personales | Archivo propio bajo %LOCALAPPDATA% (o %APPDATA%) |
Se necesita un área en la que se pueda escribir por usuario |
| Configuración por máquina (compartida entre todos los usuarios) | Número de puerto COM del dispositivo, dirección del servidor de licencias | Archivo propio bajo %ProgramData% |
El administrador la configura una vez por equipo y se comparte entre todos los usuarios |
| Secretos | Contraseña de la cadena de conexión, claves de API, tokens | Archivo protegido con DPAPI (producción), user-secrets (solo en desarrollo) | No se guardan en texto plano; no se reutiliza el mecanismo de desarrollo en producción |
| Valores que cambian durante la ejecución | Indicadores de funcionalidad, cambio dinámico del nivel de registro | appsettings.json (reloadOnChange) + IOptionsMonitor |
Se usa solo para los valores que se quiere reflejar sin reiniciar |
Con esta tabla como base, adelantamos primero la conclusión.
- La prioridad de la configuración sigue una única regla: «el proveedor añadido después gana». De forma predeterminada se cargan en el orden
appsettings.json→appsettings.{Environment}.json→ user secrets (solo en el entorno de desarrollo) → variables de entorno → argumentos de línea de comandos, y si una misma clave aparece en varios, el valor leído después sobrescribe al anterior. Con solo recordar este orden se explica la mayoría de los casos de «lo escribí en el json pero no se refleja».1 - Si usa
Host.CreateApplicationBuilder, esta jerarquía queda lista de forma predeterminada sin que tenga que montarla usted mismo. Incluso en aplicaciones de escritorio como Windows Forms o WPF, si usa Generic Host obtiene los mismos beneficios de estos valores predeterminados.23 - El cambio de entorno se realiza con
DOTNET_ENVIRONMENT(oASPNETCORE_ENVIRONMENT). En la familiaWebApplicationtiene prioridadDOTNET_ENVIRONMENT, y si ninguna de las dos está definida, el valor predeterminado esProduction. En aplicaciones de escritorio o servicios de Windows es necesario diseñar de antemano quién y cómo pasa esta variable de entorno al proceso.4 - No reciba la configuración como un DTO cualquiera: recíbala con tipos mediante la familia
IOptions<T>. UseIOptions<T>si le basta con calcularlo una sola vez al iniciar, eIOptionsMonitor<T>si quiere reflejar los cambios sin reiniciar. Si usa ambos sin entender la diferencia entre ellos, se producen tanto el accidente de «cambia un valor que no debería cambiar» como el de «no cambia un valor que debería cambiar».5 - Haga que los errores de configuración fallen al iniciar, no en tiempo de ejecución. Combinando
ValidateDataAnnotations()conValidateOnStart(), los errores de configuración se detectan «con una excepción justo después de iniciar», lo que evita el accidente de «darse cuenta de madrugada en producción por unaNullReferenceException».6 - No coloque secretos en texto plano en
appsettings.json. En desarrollo use user-secrets, y en producción, DPAPI (ProtectedData) o el Administrador de credenciales. La documentación oficial deja claro que user-secrets no está cifrado y que es un mecanismo exclusivo para desarrollo.78
2. Los fundamentos del sistema de configuración de .NET — IConfiguration y los proveedores
El sistema de configuración de .NET consiste en cargar varios proveedores de configuración superpuestos detrás de la apariencia de un único almacén clave-valor llamado IConfiguration. La ventaja de este mecanismo es que permite integrar, detrás de la misma interfaz IConfiguration, orígenes de naturaleza muy distinta: JSON, variables de entorno, argumentos de línea de comandos, INI, XML o colecciones en memoria.9
Los proveedores se apilan siguiendo una regla simple: «el que se añade después gana». Si una misma clave existe en varios proveedores, se aplica el valor del proveedor añadido en último lugar.1
Si usa Host.CreateApplicationBuilder(args), los proveedores se montan de forma predeterminada en el siguiente orden (cuanto mayor es el número, mayor la prioridad, es decir, gana el último).2
appsettings.jsonappsettings.{Environment}.json- Secret Manager (solo en el entorno de desarrollo)
- Variables de entorno
- Argumentos de línea de comandos
Este orden predeterminado es común tanto en aplicaciones de consola como en Worker Service o aplicaciones Windows Forms. A continuación se muestra un ejemplo de configuración mínima.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// De forma predeterminada, appsettings.json / appsettings.{Environment}.json / variables de entorno /
// argumentos de línea de comandos ya están cargados con la prioridad indicada arriba
string? connectionString = builder.Configuration.GetConnectionString("Main");
using IHost host = builder.Build();
await host.RunAsync();
Incluso en una aplicación de escritorio como Windows Forms, si añade el paquete Microsoft.Extensions.Hosting puede usar directamente el mismo HostApplicationBuilder. Como explicamos en nuestra entrada «¿Qué es Generic Host?», Generic Host es la base que se ocupa a la vez de la configuración, la inyección de dependencias y el registro de eventos, y el hecho de que sea una aplicación de escritorio no es motivo para renunciar a las ventajas del sistema de configuración. Para una forma concreta de organizarlo en aplicaciones con procesamiento en segundo plano, consulte también «Usar Generic Host + BackgroundService en una aplicación de escritorio».
Hay un punto de precaución en el tratamiento de las variables de entorno. La configuración del host (raíz de contenido, nombre del entorno, etc.) se lee a partir de variables de entorno con el prefijo DOTNET_, pero estas no se usan para la configuración de la aplicación (los valores generales que se leen a través de IConfiguration). Las variables de entorno que se quieren leer como configuración de la aplicación se incorporan al orden predeterminado, sin prefijo, mediante AddEnvironmentVariables().10 Si quiere usar un prefijo propio, añádalo explícitamente, como en builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_").
using Microsoft.Extensions.Configuration;
// Se añade después de los proveedores predeterminados, por lo que obtiene la prioridad más alta
builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_");
3. Configuración por entorno — appsettings.{Environment}.json
Cuando quiere cambiar la cadena de conexión o el extremo según el entorno, use archivos por entorno como appsettings.Development.json, appsettings.Staging.json o appsettings.Production.json. Es importante tener en cuenta que este archivo se trata como un archivo diferencial que «sobrescribe» al appsettings.json base. No hace falta reescribir todos los elementos: basta con escribir solo los valores que cambian según el entorno.1
El nombre del entorno se determina mediante las variables de entorno DOTNET_ENVIRONMENT o ASPNETCORE_ENVIRONMENT. Si usa WebApplication, el valor de DOTNET_ENVIRONMENT tiene prioridad sobre ASPNETCORE_ENVIRONMENT, y si ninguna de las dos está definida, el valor predeterminado es Production. En Windows no se distingue entre mayúsculas y minúsculas en el nombre de la variable de entorno, pero en Linux sí, así que si tiene en mente la contenerización, es más seguro mantener la ortografía uniforme.4
Aquí surge un problema: en las aplicaciones de escritorio o los servicios de Windows, «cómo pasar la variable de entorno» se convierte en una tarea en sí misma. Mecanismos de desarrollo como launchSettings.json de ASP.NET Core solo se usan en desarrollo local, por lo que hay que preparar aparte un medio para cambiar de entorno en producción. Las tres formas representativas de pasarla son las siguientes.
- Configurarla como variable de entorno a nivel de máquina. Si la hace persistente con algo como
setx DOTNET_ENVIRONMENT Production /M, se transmite a todos los procesos que se ejecutan en esa máquina. Sin embargo, no es adecuada si quiere que convivan en la misma máquina aplicaciones de varios entornos. - Pasar la variable de entorno al proceso de inicio del servicio de Windows. Cuando se ejecuta de forma persistente como servicio, se inicia en un contexto distinto al de la sesión del usuario interactivo, por lo que no se heredan las variables de entorno del usuario. Para los detalles de la cuenta de ejecución y la separación de sesiones, consulte «Cómo crear y operar servicios de Windows». Es realista usar una variable de entorno a nivel de máquina, o bien convertir el ejecutable del servicio en un script contenedor delgado que haga
SETantes de iniciar el ejecutable principal (el contenido del script se muestra más abajo). - Si se inicia a través del Programador de tareas, es más fiable pasarla como argumento de línea de comandos. Como la pestaña «Acciones» del Programador de tareas no ofrece una interfaz para configurar variables de entorno directamente, se producen menos incidentes si se pasa como un argumento de línea de comandos, por ejemplo
--environment Production, y se lee conAddCommandLine(args). Las peculiaridades propias del Programador de tareas en cuanto a la cuenta de ejecución y el tipo de inicio de sesión están recogidas en «Las tareas del Programador de tareas no se ejecutan y terminan con 0x1».
El script contenedor mencionado en segundo lugar se resuelve con las siguientes tres líneas.
@echo off
set DOTNET_ENVIRONMENT=Production
"%~dp0MyApp.Service.exe" %*
%~dp0 es la carpeta en la que se encuentra el propio script (termina con \), de modo que puede iniciar el ejecutable vecino sin importar cuál sea el directorio de trabajo. Como set solo afecta a este proceso y a sus procesos hijos, no interfiere aunque conviva en la misma máquina con aplicaciones de otro entorno. Sin embargo, este script en sí no se puede registrar directamente como servicio con sc create. Solo se puede registrar como servicio un ejecutable que responda al control de servicios; si se especifica un script por lotes, se produce un tiempo de espera agotado al iniciar (error 1053). Este método solo sirve cuando se inicia a través de un contenedor de servicio o, como se indicó a continuación, desde el Programador de tareas. Si quiere registrar directamente el propio servicio, elija la variable de entorno a nivel de máquina o el método de argumentos de línea de comandos.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
var options = new HostApplicationBuilderSettings
{
Args = args,
// Para las rutas de ejecución en las que no se pasa la variable de entorno (como el Programador de tareas),
// también se admite el cambio de entorno mediante argumentos de línea de comandos
};
HostApplicationBuilder builder = Host.CreateApplicationBuilder(options);
Console.WriteLine($"Entorno actual: {builder.Environment.EnvironmentName}");
4. El patrón de opciones — recibir la configuración con tipos
Leer con claves de cadena como IConfiguration["Key:SubKey"] tiene la debilidad de que no se detectan los errores de tipeo y resulta difícil de seguir cuando el anidamiento se hace profundo. En la práctica, lo habitual es usar el patrón de opciones, que vincula la configuración a un POCO y la recibe con tipos.
public sealed class ExternalApiOptions
{
public const string SectionName = "ExternalApi";
public required string BaseUrl { get; set; }
public required string ApiKey { get; set; }
public int TimeoutSeconds { get; set; } = 30;
}
El registro y el enlace se escriben así.
using Microsoft.Extensions.DependencyInjection;
builder.Services
.AddOptions<ExternalApiOptions>()
.Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName));
Hay tres interfaces para recibir la configuración, cada una con características distintas.5
| Interfaz | Duración del registro | Reflejo de cambios de configuración | Uso principal |
|---|---|---|---|
IOptions<T> |
Singleton | No se refleja (se calcula una sola vez al iniciar) | Configuración que se asume invariable. La más simple |
IOptionsSnapshot<T> |
Con ámbito (scoped) | Se recalcula cada vez que se reconstruye el ámbito | Contextos con un ámbito claro, como el ámbito de solicitud web |
IOptionsMonitor<T> |
Singleton | Obtiene siempre el valor más reciente, con notificación de cambios (OnChange) |
Contextos en los que se quiere detectar el cambio al instante, como servicios persistentes |
En aplicaciones que no tienen un ámbito de solicitud HTTP, como las de escritorio o los servicios de Windows, aunque use IOptionsSnapshot<T>, mientras no cree usted mismo el ámbito, el comportamiento es en la práctica igual al de IOptions<T>. Se reducen las dudas si adopta el criterio de usar directamente IOptionsMonitor<T> cuando quiere detectar cambios.
Los valores de configuración (BaseUrl, ApiKey, tiempo de espera) se obtienen de IOptionsMonitor en cada llamada, pero el propio HttpClient se obtiene de IHttpClientFactory y se reutiliza. Si crea con new y elimina con Dispose el HttpClient en cada llamada, se destruye y reconstruye el grupo de sockets y conexiones interno cada vez, lo que en sondeos de alta frecuencia o procesamiento por lotes puede llevar al agotamiento de puertos efímeros. Por eso, aunque los valores cambien, lo habitual es dejar la reutilización de la conexión en manos de IHttpClientFactory.
using Microsoft.Extensions.Options;
public sealed class ExternalApiClient(
IHttpClientFactory httpClientFactory,
IOptionsMonitor<ExternalApiOptions> optionsMonitor)
{
public async Task<string> FetchAsync(CancellationToken cancellationToken)
{
// Obtiene el valor más reciente en cada llamada; si el archivo de configuración se actualizó, se refleja
ExternalApiOptions current = optionsMonitor.CurrentValue;
// El propio HttpClient se obtiene a través de la fábrica y reutiliza el grupo de conexiones interno
HttpClient client = httpClientFactory.CreateClient(nameof(ExternalApiClient));
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(new Uri(current.BaseUrl), "status"));
request.Headers.Add("X-Api-Key", current.ApiKey);
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(current.TimeoutSeconds));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
using HttpResponseMessage response = await client.SendAsync(request, linkedCts.Token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
}
En el lado que realiza la llamada, registre un cliente con nombre como builder.Services.AddHttpClient(nameof(ExternalApiClient));. La división de responsabilidades es: los valores que pueden cambiar con la configuración, como BaseUrl o ApiKey, se incorporan a HttpRequestMessage en cada solicitud, mientras que el coste de creación y destrucción de la propia instancia de HttpClient queda a cargo de la fábrica.
Cuando un error de configuración aparece como una excepción en tiempo de ejecución, la investigación se alarga. Combinando la validación con DataAnnotations y ValidateOnStart(), puede diseñar la aplicación de forma que una aplicación con la configuración rota directamente no pueda iniciarse.6
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
public sealed class ExternalApiOptions
{
public const string SectionName = "ExternalApi";
[Required, Url]
public required string BaseUrl { get; set; }
[Required, MinLength(16)]
public required string ApiKey { get; set; }
[Range(1, 300)]
public int TimeoutSeconds { get; set; } = 30;
}
builder.Services
.AddOptions<ExternalApiOptions>()
.Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart(); // La validación se ejecuta al iniciar el host (en StartAsync/RunAsync), antes de que arranquen los servicios del host
Si no añade ValidateOnStart(), la validación se retrasa hasta el momento en que realmente se accede por primera vez a esa opción. La validación en sí no se ejecuta justo después de Build(), sino al iniciar el host (StartAsync/RunAsync), así que tenga en cuenta que, por ejemplo, en código de prueba que solo llama a Build() sin llegar a llamar a RunAsync(), la validación todavía no se habrá ejecutado en ese punto. Para evitar el accidente intermitente de «la aplicación inició, pero se cae en el instante en que se abre la pantalla que usa esa configuración», añada ValidateOnStart() como norma general en la configuración de las aplicaciones de negocio.6
5. Dónde colocar la configuración que se puede escribir
appsettings.json es el lugar para colocar «valores predeterminados de solo lectura», no el lugar donde la propia aplicación reescribe su configuración. Muchas aplicaciones de negocio se instalan bajo Program Files, donde los usuarios estándar no tienen permiso de escritura, por lo que se produce el accidente de una excepción en tiempo de ejecución o de que el contenido visible difiera por usuario debido a la virtualización del sistema de archivos de Windows.
La configuración que necesita escritura se separa según su naturaleza como se indica a continuación. Es más seguro basarse en las carpetas especiales que se obtienen con Environment.GetFolderPath.11
| Ubicación | Forma de obtenerla | Uso |
|---|---|---|
%LOCALAPPDATA%\NombreEmpresa\NombreApp |
Environment.SpecialFolder.LocalApplicationData |
Ubicación predeterminada de la configuración y los datos por usuario |
%APPDATA%\NombreEmpresa\NombreApp (Roaming) |
Environment.SpecialFolder.ApplicationData |
Solo la configuración que se quiere que siga al usuario en un entorno de perfiles móviles |
%ProgramData%\NombreEmpresa\NombreApp |
Environment.SpecialFolder.CommonApplicationData |
Configuración por máquina compartida entre todos los usuarios. Requiere diseñar las ACL |
using System;
using System.IO;
using System.Text.Json;
public sealed class UserSettingsStore
{
private readonly string _filePath;
public UserSettingsStore()
{
string root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string dir = Path.Combine(root, "YourCompany", "MyApp");
Directory.CreateDirectory(dir);
_filePath = Path.Combine(dir, "user-settings.json");
}
public UserSettings Load()
{
if (!File.Exists(_filePath))
{
return new UserSettings();
}
string json = File.ReadAllText(_filePath);
return JsonSerializer.Deserialize<UserSettings>(json) ?? new UserSettings();
}
public void Save(UserSettings settings)
{
string json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
// Se asume que la serialización de escrituras múltiples dentro del mismo proceso corre a cargo de quien la invoca
File.WriteAllText(_filePath, json);
}
}
public sealed class UserSettings
{
public string? LastOpenedFolder { get; set; }
public int WindowWidth { get; set; } = 1024;
public int WindowHeight { get; set; } = 768;
}
Si mezcla la configuración por usuario y por máquina en un solo archivo, en un entorno donde varios usuarios comparten la misma máquina se produce el accidente de que «la configuración del usuario A perjudica al usuario B». Para entender la separación por usuario y por máquina, y la propia estructura de perfiles de Windows, consulte «Cómo funcionan los perfiles de usuario de Windows»; para una tabla de decisión general sobre cómo elegir la ubicación de almacenamiento, consulte también «Cómo elegir la ubicación de almacenamiento de datos locales de una aplicación Windows».
6. Secretos — cadenas de conexión y claves de API
Escribir la contraseña de la cadena de conexión o la clave de API directamente en appsettings.json es un riesgo en la práctica, aunque lo haya incluido en .gitignore. El texto plano puede filtrarse por vías como la distribución en una carpeta compartida, el envío del archivo para soporte técnico, o un archivo de configuración antiguo «zombi» que permanece en una copia de seguridad.
En desarrollo use Secret Manager (dotnet user-secrets). Sin embargo, este es un mecanismo pensado para la experiencia de desarrollo y no está cifrado. Los valores se guardan en JSON en texto plano en %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json, y la documentación oficial deja explícito que «no debe tratarse como un almacén de confianza; es exclusivo para desarrollo». Es un error usar user-secrets para distribuir secretos de producción.7
dotnet user-secrets init
dotnet user-secrets set "ExternalApi:ApiKey" "valor de desarrollo"
En producción, use DPAPI (Data Protection API), que ofrece Windows, para cifrar vinculando el secreto a las credenciales del usuario o de la máquina. Protect/Unprotect de System.Security.Cryptography.ProtectedData actúan como envoltorio, y si usa DataProtectionScope.CurrentUser, solo se puede descifrar iniciando sesión con la misma cuenta de usuario y únicamente en la misma máquina. Tenga en cuenta al diseñar que DPAPI es una función exclusiva de Windows y en otras plataformas produce PlatformNotSupportedException.8
using System.Security.Cryptography;
using System.Text;
public static class SecretProtector
{
// Mezclar una entropía que indique el propósito reduce la posibilidad de confundirla con datos protegidos para otros fines
private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("MyApp.ExternalApi.ApiKey");
public static string Protect(string plainText)
{
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] protectedBytes = ProtectedData.Protect(plainBytes, Entropy, DataProtectionScope.CurrentUser);
return Convert.ToBase64String(protectedBytes);
}
public static string Unprotect(string protectedBase64)
{
byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
byte[] plainBytes = ProtectedData.Unprotect(protectedBytes, Entropy, DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(plainBytes);
}
}
A continuación, una aclaración sobre el papel de la entropía (optionalEntropy) usada en el código anterior. El ámbito CurrentUser de DPAPI es una protección de tipo «se puede descifrar si es el mismo usuario y la misma máquina», así que si no especifica una entropía, otras aplicaciones que se ejecuten con los permisos del mismo usuario también podrían descifrarlo. La documentación oficial deja claro que, si cifró pasando una entropía, también hay que pasar el mismo valor al descifrar, o de lo contrario no se puede descifrar.8 Es decir, la entropía es una contraseña propia de la aplicación para evitar «el descifrado por parte de otras aplicaciones que se ejecutan con el mismo usuario en la misma máquina». Sin embargo, entienda que, como esa contraseña queda incrustada dentro del propio ejecutable, se trata de una defensa que puede leerse si se analiza el binario (no es un medio para impedir el descifrado por parte del propio usuario).
Los detalles a nivel de implementación —dónde guardar el valor protegido con DPAPI, qué ámbito elegir entre CurrentUser y LocalMachine, o las trampas en aplicaciones de negocio que usan varios usuarios en la misma máquina— están tratados en profundidad en «Almacenamiento de información confidencial en aplicaciones Windows: evitar la configuración en texto plano con DPAPI», así que consúltela en el momento de implementar.
La línea entre lo que se puede dejar en texto plano y lo que no es simple. Se juzga por si, cuando ese valor se filtra, hace falta reemitir contraseñas o claves de API, investigar un acceso no autorizado o informar a un organismo regulador. El nombre de host o el número de puerto de una cadena de conexión suelen tener un daño real pequeño si se filtran, mientras que la contraseña o el token de autenticación incrustados en ella son siempre objeto de protección. Si construye la cadena de conexión separando la «información del servidor» de las «credenciales», y diseña que solo estas últimas sean objeto de protección con DPAPI, puede separar a nivel de código la parte que puede quedar en texto plano con poco daño real de la que debe protegerse.
7. Cambios de configuración durante la ejecución — la realidad de reloadOnChange
La llamada predeterminada a AddJsonFile (la que hace internamente Host.CreateApplicationBuilder) tiene reloadOnChange: true, por lo que appsettings.json y appsettings.{Environment}.json detectan cambios en el archivo y se recargan automáticamente. La implementación consiste en que PhysicalFileProvider usa internamente FileSystemWatcher para vigilar los cambios.12
Este mecanismo tiene varios límites prácticos.
- Solo se refleja en los valores leídos a través de
IOptionsMonitor<T>(yIOptionsSnapshot<T>).IOptions<T>conserva el valor de cuando se inició, así que aunque reescriba el archivo, se queda con el valor antiguo. Muchas de las consultas del tipo «edité directamente el archivo de configuración pero no se refleja» tienen su causa en confundir estas dos interfaces. FileSystemWatcherpuede no enviar de forma fiable la notificación de cambios en sistemas de archivos como los contenedores Docker o las unidades compartidas de red. En esos entornos, si pone la variable de entornoDOTNET_USE_POLLING_FILE_WATCHERen1otrue, se cambia a una vigilancia por sondeo (polling) con un intervalo de 4 segundos (el intervalo no se puede modificar).13 Cabe señalar que esta especificación de «4 segundos, no modificable» se confirmó, en el momento de escribir este artículo (julio de 2026), en la descripción de Microsoft Learn «Options pattern in .NET» (actualizada en octubre de 2025). Como esa documentación no indica una versión concreta, si va a basar en ella una configuración de operación a largo plazo, verifique la versión más reciente de esa misma página. Las peculiaridades generales del propioFileSystemWatcher—eventos perdidos, desbordamiento del búfer, eventos duplicados— están recogidas en «Guía práctica de FileSystemWatcher».- La notificación de cambio del archivo de configuración puede dispararse varias veces por un único cambio del archivo. Si en la aplicación implementa algo como «rehacer un proceso pesado al detectar un cambio», hay que tener cuidado de aplicar una técnica de debounce para los disparos consecutivos en un intervalo corto, o de comparar el hash del contenido del archivo para procesar solo los cambios reales.
No siempre hay que aspirar a «reflejar los cambios de configuración sin reiniciar». Los valores ligeros, como el nivel de registro o los indicadores de funcionalidad, pueden reflejarse de inmediato con IOptionsMonitor, pero para valores como la cadena de conexión a la base de datos o el tamaño del grupo de subprocesos —que, al cambiar en el instante, pueden entrar en contradicción con los recursos en funcionamiento— es más seguro declarar explícitamente como especificación que «se refleja con un reinicio». Diseñar de forma que se pueda comunicar con claridad al personal de operaciones «esta configuración surte efecto en cuanto se guarda» o «esta configuración requiere reiniciar» es más importante que la simple decisión de usar o no reloadOnChange.
8. Mapa de migración desde app.config / Settings.settings
Al migrar a .NET una aplicación de la era .NET Framework, también hace falta rehacer el mecanismo de configuración. A continuación se organiza la correspondencia.14
| .NET Framework | .NET | Notas |
|---|---|---|
<appSettings> de App.config / Web.config |
appsettings.json + IConfiguration |
La estructura jerárquica se expresa de forma natural con anidamiento en JSON |
ConfigurationManager.AppSettings["Key"] |
builder.Configuration["Key"] o IOptions<T> |
Del acceso por cadena al acceso con tipos |
ConfigurationManager.ConnectionStrings |
builder.Configuration.GetConnectionString("Name") |
Se mantiene la convención de la sección ConnectionStrings |
Settings.settings (ámbito de usuario) |
Archivo JSON propio bajo %LOCALAPPDATA% |
No existe un mecanismo de generación automática como ApplicationSettingsBase; hay que serializar y guardar uno mismo (el ejemplo de implementación de la clase de guardado está en el capítulo 5) |
Settings.settings (ámbito de aplicación) |
appsettings.json |
Se trata como un valor predeterminado de solo lectura |
Cifrado de <connectionStrings> (aspnet_regiis, etc.) |
DPAPI (ProtectedData) |
Cambia el propio mecanismo de cifrado; hay que rehacerlo en la migración |
Hay dos trampas frecuentes al migrar.
La primera es que, si añade el paquete NuGet System.Configuration.ConfigurationManager, el código de lectura de App.config sigue funcionando tal cual. Es un recurso válido como primera etapa de la migración, pero no debe dejarlo así sin más: hay que incluir en el plan la migración a appsettings.json. Como las bibliotecas periféricas, tales como los proveedores de registro de eventos, también están migrando en bloque hacia dar por sentado appsettings.json, cada vez tiene menos sentido mantener el mecanismo antiguo solo para leer App.config.14
La segunda es la configuración de ámbito de usuario de Settings.settings. En .NET Framework existía un mecanismo que guardaba automáticamente la configuración por usuario con solo llamar a Properties.Settings.Default.Save(), pero .NET no incluye de forma estándar un mecanismo de generación automática equivalente. En la migración, hace falta preparar una clase de guardado propia como la mostrada en el capítulo 5.
Para otro aspecto de la migración —la visión general de los puntos que hay que verificar además de la gestión de configuración, como la compatibilidad de las bibliotecas de las que depende, la existencia de integración COM o la revisión del método de distribución— hemos reunido los criterios de inventario en «Lista de verificación previa a la migración de .NET Framework a .NET», así que recomendamos revisarla una vez en la etapa inicial del proyecto de migración.
Resumen
La gestión de configuración de .NET se compone de la combinación de tres mecanismos: la superposición de proveedores mediante IConfiguration, la recepción con seguridad de tipos mediante la familia IOptions, y el cambio de entorno mediante variables de entorno. Hasta aquí se puede usar igual en la mayoría de las aplicaciones, pero las preocupaciones propias de las aplicaciones de negocio Windows se concentran en lo que viene después: dónde colocar la configuración escribible, cómo proteger los secretos, hasta dónde permitir los cambios de configuración durante la ejecución, y cómo cerrar los activos de la generación de app.config.
Dar un paso más allá del estado de «todo está escrito en appsettings.json» y separar la ubicación y el tratamiento según el tipo de configuración: esperamos que la tabla de decisión de este artículo sirva de punto de partida para esa organización. Tanto el inventario de la gestión de configuración de una aplicación existente como la consulta sobre la estrategia de migración desde app.config son casos en los que a menudo la solución óptima solo se ve mirando el archivo de configuración real y el entorno de despliegue, así que, si tiene dudas, consúltenos.
Artículos relacionados
- ¿Qué es Generic Host?
- Usar Generic Host + BackgroundService en una aplicación de escritorio
- Las tareas del Programador de tareas no se ejecutan y terminan con 0x1
- Cómo crear y operar servicios de Windows
- Cómo elegir la ubicación de almacenamiento de datos locales de una aplicación Windows
- Cómo funcionan los perfiles de usuario de Windows
- Almacenamiento de información confidencial en aplicaciones Windows: evitar la configuración en texto plano con DPAPI
- Guía práctica de FileSystemWatcher
- Lista de verificación previa a la migración de .NET Framework a .NET
Áreas de consultoría relacionadas
KomuraSoft LLC ofrece consultoría técnica sobre el diseño de la gestión de configuración de aplicaciones de negocio Windows, el diseño de operación de la configuración por entorno y la estrategia de migración desde activos existentes de app.config.
Referencias
-
Microsoft Learn, Configuration in .NET - Alternative hosting approach. Sobre el orden de prioridad de los proveedores de configuración que monta de forma predeterminada
Host.CreateApplicationBuilder(argumentos de línea de comandos → variables de entorno → user secrets del entorno de desarrollo → appsettings.{Environment}.json → appsettings.json). ↩ ↩2 ↩3 -
Microsoft Learn, .NET Generic Host - Host builder settings. Sobre el orden predeterminado de la configuración del host (variables de entorno con prefijo
DOTNET_, argumentos de línea de comandos) y la configuración de la aplicación (appsettings.json, appsettings.{Environment}.json, Secret Manager, variables de entorno, argumentos de línea de comandos) que cargaHost.CreateApplicationBuilder. ↩ ↩2 -
Microsoft Learn, Use the .NET Generic Host in a Windows Forms app. Sobre el procedimiento para incorporar Generic Host en una aplicación Windows Forms y usar la inyección de dependencias, la configuración y el registro de eventos. ↩
-
Microsoft Learn, ASP.NET Core runtime environments - Environment variables that determine the runtime environment. Sobre la relación entre
DOTNET_ENVIRONMENTyASPNETCORE_ENVIRONMENT, la prioridad deDOTNET_ENVIRONMENTal usarWebApplication, el valor predeterminadoProductioncuando no está definida, y que en Windows el nombre de la variable de entorno no distingue mayúsculas y minúsculas mientras que en Linux sí. ↩ ↩2 -
Microsoft Learn, Options pattern in .NET - Options interfaces. Sobre las diferencias de duración, el momento en que se reflejan los cambios de configuración y las funciones admitidas entre
IOptions<TOptions>,IOptionsSnapshot<TOptions>eIOptionsMonitor<TOptions>. ↩ ↩2 -
Microsoft Learn, Options pattern in .NET - Options validation. Sobre la validación con DataAnnotations mediante
ValidateDataAnnotations()y la configuración de la validación al iniciar medianteValidateOnStart()(oAddOptionsWithValidateOnStart). ↩ ↩2 ↩3 -
Microsoft Learn, Safe storage of app secrets in development in ASP.NET Core - Use the Secret Manager tool. Sobre el hecho de que Secret Manager no cifra los secretos y los guarda en texto plano en
%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json, y que es exclusivo para desarrollo y no debe tratarse como un almacén de confianza. ↩ ↩2 -
Microsoft Learn, ProtectedData Class. Sobre los métodos
ProtectedData.Protect/Unprotect, que envuelven DPAPI (Data Protection API), la diferencia de ámbito entreDataProtectionScope.CurrentUser/LocalMachine, y el hecho de que es exclusivo de Windows y en otras plataformas producePlatformNotSupportedException. ↩ ↩2 ↩3 -
Microsoft Learn, Configuration providers in .NET. Sobre el mecanismo de los proveedores de configuración que integran detrás de
IConfigurationorígenes de naturaleza distinta, como JSON, variables de entorno, línea de comandos, INI y XML. ↩ -
Microsoft Learn, Configuration providers in .NET - Environment variable configuration provider. Sobre el hecho de que la configuración predeterminada carga las variables de entorno con prefijo
DOTNET_y los argumentos de línea de comandos en la configuración del host y de la aplicación, pero no en la configuración de usuario, y sobre cómo añadir un prefijo personalizado. ↩ -
Microsoft Learn, Environment.GetFolderPath Method. Sobre la forma de obtener rutas de carpetas especiales mediante la enumeración
Environment.SpecialFoldery el métodoGetFolderPath. ↩ -
Microsoft Learn, Detect changes with change tokens in ASP.NET Core - Monitor for configuration changes. Sobre el parámetro
reloadOnChangedeAddJsonFiley el mecanismo por el quePhysicalFileProviderusa internamenteFileSystemWatcherpara vigilar los cambios del archivo de configuración. ↩ -
Microsoft Learn, Options pattern in .NET - IOptionsMonitor. Sobre el hecho de que la notificación de cambios de
IOptionsMonitorse limita a los proveedores de configuración basados en el sistema de archivos, y que en contenedores Docker o unidades compartidas de red, donde la notificación de cambios no es fiable, se puede cambiar a una vigilancia por sondeo de 4 segundos con la variable de entornoDOTNET_USE_POLLING_FILE_WATCHER. ↩ -
Microsoft Learn, Modernize after upgrading to .NET from .NET Framework - App.config. Sobre el procedimiento de migración de
App.configaappsettings.json, el mantenimiento de la compatibilidad mediante el paquete NuGetSystem.Configuration.ConfigurationManagery el uso del paqueteMicrosoft.Extensions.Configuration.Json. ↩ ↩2
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Internacionalización de aplicaciones WinForms/WPF — la práctica de resx, ensamblados satélite y el cambio de cultura
Organizamos la internacionalización de aplicaciones de escritorio Windows: la diferencia entre CurrentCulture y CurrentUICulture, el meca...
Identificar qué es «lento» con PerfView y dotnet-trace — introducción práctica a la investigación de rendimiento en .NET
Qué herramientas y datos conviene mirar cuando una aplicación de negocio va lenta, mantiene la CPU al máximo o se queda congelada ocasion...
Impresión y salida en PDF en aplicaciones empresariales de Windows ── cómo elegir entre System.Drawing.Printing, WPF y bibliotecas de informes
Organiza en una tabla de decisión por requisitos la impresión WinForms con PrintDocument, la impresión WPF con FlowDocument o FixedDocume...
El CSV no es «solo texto»: guía práctica del manejo de CSV en aplicaciones empresariales con C# (codificación de caracteres, compatibilidad con Excel, protección contra inyección)
Repasamos los incidentes típicos de entrada/salida de CSV en aplicaciones empresariales -el parseo casero con Split(','), la corrupción d...
No envuelvas HttpClient en un using ── Comunicación HTTP en aplicaciones de negocio C# (patrones de creación, tiempos de espera y reintentos)
Crear HttpClient con using en cada solicitud agota los sockets, y usarlo como static no sigue los cambios de DNS. Repasamos el patrón de ...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Generic Host y arquitectura de aplicaciones
Generic Host, BackgroundService, DI, configuración, registro y ciclo de vida de la aplicación.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
El diseño de la gestión de configuración y de los archivos de ajustes forma parte de las consultas prácticas de desarrollo de aplicaciones Windows.
Consultoría técnica y revisión de diseño
Decidir la estrategia de migración desde un app.config existente corresponde a la consultoría técnica con revisión de diseño.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Puedo dejar appsettings.json en la misma carpeta que el exe?
- No hay problema si contiene valores predeterminados de solo lectura. Pero si esa carpeta se instala bajo Program Files, la aplicación no puede diseñarse para reescribir appsettings.json ella misma: los usuarios estándar no tienen permiso de escritura en Program Files, y se producirán excepciones en tiempo de ejecución o, por la función de virtualización del sistema de archivos de Windows, el contenido visible variará según el usuario. Separe los ajustes que requieren escritura en otro archivo bajo %LOCALAPPDATA% o %ProgramData%.
- ¿Debo priorizar las variables de entorno o appsettings.json?
- Lo básico es no cambiar el orden de prioridad predeterminado y usarlo tal cual: appsettings.json → appsettings.{Environment}.json → variables de entorno → argumentos de línea de comandos, donde cada fuente posterior sobrescribe a la anterior. El criterio cuando hay duda es la «naturaleza del valor»: si establece que los valores predeterminados que pueden incluirse en el artefacto de compilación van en appsettings.json, y los valores que cambian según el destino de despliegue o el contenedor van en variables de entorno, no habrá dudas al revisarlo más adelante.
- ¿Cómo debo dividir las clases de configuración?
- Lo básico es separar las clases de opciones por función o responsabilidad. Por ejemplo, ConnectionOptions para las cadenas de conexión y ExternalApiOptions para la integración con una API externa, evitando amontonar ajustes no relacionados en una sola clase. Al separarlas, la unidad de validación y recarga con IOptionsSnapshot/IOptionsMonitor también se divide de forma natural, y la validación con DataAnnotations queda contenida dentro de cada clase.
- ¿Debo migrar desde archivos INI o el registro?
- Si va a rehacer la gestión de configuración desde cero, recomendamos unificarla en appsettings.json + IConfiguration. .NET también ofrece un proveedor de configuración INI, por lo que puede seguir leyendo el archivo INI existente y migrar de forma gradual. El registro de Windows no forma parte del alcance estándar de los proveedores de configuración de .NET, así que si tiene activos existentes que dependen de él, lo realista es escribir código puente de solo lectura e ir acercándolos gradualmente hacia appsettings.json.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.