Variables de entorno en C#: leerlas, cambiarlas y configuración con tipos

Las variables de entorno permiten configurar un programa (el puerto, la base de datos, el modo) sin cambiar su código ni recompilarlo. En C# se leen con una línea, y la biblioteca de configuración de .NET las convierte en objetos con tipos.

Leer una variable

Las variables de entorno son la forma estándar de pasar configuración a un programa sin tocar su código: el puerto, la cadena de conexión de la base de datos, el modo de depuración. Para esta página, el programa se ejecutó con cuatro variables definidas, como si se hubieran escrito antes en la terminal:

APP_NOMBRE=Tienda APP_PUERTO=8080 APP_BASEDATOS__RUTA=/datos/tienda.db APP_BASEDATOS__ESPERA=30 dotnet run leer.cs

(En Linux y macOS se escriben así, delante del comando. En PowerShell, con $env:APP_PUERTO = "8080" antes de ejecutar.)

string nombre = Environment.GetEnvironmentVariable("APP_NOMBRE") ?? "Mi aplicación";
string modo = Environment.GetEnvironmentVariable("APP_MODO") ?? "produccion";

string? textoPuerto = Environment.GetEnvironmentVariable("APP_PUERTO");
int puerto = int.TryParse(textoPuerto, out int p) ? p : 5000;

Console.WriteLine($"{nombre} en modo {modo}, puerto {puerto}");
salidaTienda en modo produccion, puerto 8080

Environment.GetEnvironmentVariable devuelve un string?: null si la variable no existe, como APP_MODO. Con ?? se le da un valor por omisión, y los números hay que convertirlos, porque todas las variables de entorno son texto.

Ver todas

using System.Collections;

var nuestras = Environment.GetEnvironmentVariables()
    .Cast<DictionaryEntry>()
    .Where(e => ((string)e.Key).StartsWith("APP_"))
    .OrderBy(e => (string)e.Key, StringComparer.Ordinal);

foreach (var e in nuestras)
{
    Console.WriteLine($"{e.Key} = {e.Value}");
}
salidaAPP_BASEDATOS__ESPERA = 30
APP_BASEDATOS__RUTA = /datos/tienda.db
APP_NOMBRE = Tienda
APP_PUERTO = 8080
APP_UID = 1654

GetEnvironmentVariables() devuelve todas, en un IDictionary antiguo, sin tipos (de ahí el Cast<DictionaryEntry>). Filtrar por un prefijo evita mezclarlas con las del sistema… aunque aquí se coló una quinta, APP_UID, que no es nuestra: la definen las imágenes oficiales de .NET para Docker, en las que se ejecutó este ejemplo. APP_ es un prefijo demasiado común; en una aplicación real, usa uno propio, como su nombre (TIENDA_).

Cambiarlas desde el programa

Console.WriteLine(Environment.GetEnvironmentVariable("APP_TEMPORAL") ?? "(no existe)");

Environment.SetEnvironmentVariable("APP_TEMPORAL", "solo para este proceso");
Console.WriteLine(Environment.GetEnvironmentVariable("APP_TEMPORAL"));

Environment.SetEnvironmentVariable("APP_TEMPORAL", null);   // null la borra
Console.WriteLine(Environment.GetEnvironmentVariable("APP_TEMPORAL") ?? "(borrada)");
salida(no existe)
solo para este proceso
(borrada)

SetEnvironmentVariable solo cambia el entorno de tu proceso y de los programas que lance a partir de ese momento. No cambia la terminal desde la que lo ejecutaste ni el resto del sistema: al terminar el programa, la variable desaparece.

Configuración con tipos: Microsoft.Extensions.Configuration

En aplicaciones más grandes, leer variables una a una se vuelve pesado. La biblioteca de configuración de .NET, la misma que usa ASP.NET Core, junta varias fuentes (valores por omisión, un appsettings.json, el entorno) y las vuelca en una clase:

#:package Microsoft.Extensions.Configuration.EnvironmentVariables@10.0.14
#:package Microsoft.Extensions.Configuration.Binder@10.0.14
#:property PublishAot=false
using Microsoft.Extensions.Configuration;

var configuracion = new ConfigurationBuilder()
    .AddInMemoryCollection(new Dictionary<string, string?>
    {
        ["Puerto"] = "5000",                 // valores por omisión
        ["BaseDatos:Espera"] = "15",
    })
    .AddEnvironmentVariables(prefix: "APP_") // lo que llegue del entorno manda
    .Build();

var opciones = configuracion.Get<Opciones>()!;
Console.WriteLine($"{opciones.Nombre}, puerto {opciones.Puerto}");
Console.WriteLine($"Base de datos en {opciones.BaseDatos.Ruta}, espera {opciones.BaseDatos.Espera} s");

class Opciones
{
    public string Nombre { get; set; } = "";
    public int Puerto { get; set; }
    public OpcionesBaseDatos BaseDatos { get; set; } = new();
}

class OpcionesBaseDatos
{
    public string Ruta { get; set; } = "";
    public int Espera { get; set; }
}
salidaTienda, puerto 8080
Base de datos en /datos/tienda.db, espera 30 s

Las fuentes se añaden en orden y la última manda: el puerto 8080 del entorno sustituye al 5000 por omisión. AddEnvironmentVariables(prefix: "APP_") toma solo las que empiezan por APP_ y quita el prefijo. El doble guion bajo separa niveles: APP_BASEDATOS__RUTA llega como BaseDatos:Ruta y va a parar a opciones.BaseDatos.Ruta. Las mayúsculas no importan al emparejar. PublishAot=false hace falta en un archivo suelto porque Get<T>() usa reflexión, como explica la página de JSON en C#.

Mayúsculas y secretos. En Linux y macOS, APP_PUERTO y app_puerto son variables distintas; en Windows, la misma. Usa siempre mayúsculas. Y aunque las variables de entorno son el sitio habitual para contraseñas y claves de API (mejor que escribirlas en el código), las heredan los procesos hijos y pueden acabar en registros de errores: nunca las imprimas enteras, y en desarrollo usa los user secrets de .NET (dotnet user-secrets).

Todo el código de esta página se compiló con el .NET SDK 10.0.403 (C# 14) y se ejecutó con .NET 10.0.14 en un contenedor limpio antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.