Guía de C# y .NET: De 0 a 100
¿Qué es C# y .NET?
C# es un lenguaje de programación tipado estáticamente, multiparadigma, creado por Anders Hejlsberg en Microsoft y lanzado en 2000 como parte de la iniciativa .NET. Su diseño combina la potencia de C++ con la simplicidad de Visual Basic, incorporando características funcionales, orientadas a objetos y de programación asíncrona en un mismo lenguaje.
.NET es el ecosistema: primero fue .NET Framework (solo Windows), luego .NET Core (multiplataforma, open-source, 2016), y desde .NET 5 (2020) es una plataforma unificada que reemplaza ambos. Hoy estamos en .NET 9 (2024), con releases anuales en noviembre.
¿Dónde se usa?
- Web: ASP.NET Core — potencian Stack Overflow, Microsoft, GoDaddy, JetStream
- Juegos: Unity (C# scripting), Godot (C# bindings) — estándar en desarrollo indie
- Desktop/ MAUI: .NET MAUI, WPF, WinForms — aplicaciones nativas Windows/macOS/iOS/Android
- Cloud / Microservicios: Azure Functions, AWS Lambda con .NET, microservicios con gRPC
- Bases de datos: Entity Framework Core, Dapper — ORM y micro-ORM estándar
- Machine Learning: ML.NET, TorchSharp, TensorFlow.NET
- IoT: .NET nanoFramework, Windows IoT, Raspberry Pi con .NET
- CLI / DevOps: PowerShell está escrito en C#, herramientas cross-platform
¿Quién lo usa? Stack Overflow (toda su plataforma), Unity Technologies (motor de juegos), JetBrains (Rider, ReSharper), GoDaddy (hosting), Siemens (automación industrial), Dell (aplicaciones empresariales).
Fuente: Microsoft .NET Docs, C# docs, .NET GitHub
Prerrequisitos
Antes de empezar, necesitas:
- Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
- El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)
Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.
Instalación en todos los entornos
Windows
- Descarga el instalador desde dotnet.microsoft.com/download
- Ejecuta el instalador MSI — incluye SDK, runtime, CLI y ASP.NET Core
- Verifica: abre
cmdo PowerShell y ejecutadotnet --version - Alternativa vía winget:
winget install Microsoft.DotNet.SDK.9 - Visual Studio: instala Community Edition desde visualstudio.microsoft.com — selecciona carga de trabajo “Desarrollo de .NET” y “ASP.NET y desarrollo web”
- JetBrains Rider: IDE de pago con soporte nativo .NET (30 días trial)
macOS
- Instalador oficial: descarga .pkg desde dotnet.microsoft.com
- Homebrew:
brew install dotnet-sdk - Visual Studio for Mac (legacy): Microsoft anunció su retiro para 2024, migrar a VS Code + C# Dev Kit o Rider
- Verifica:
dotnet --version
Linux (Debian/Ubuntu)
# Añadir repositorio Microsoft (necesario para apt)
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt update
sudo apt install dotnet-sdk-9.0 aspnetcore-runtime-9.0
# Fedora/RHEL
sudo dnf install dotnet-sdk-9.0
SDK de versiones múltiples (no necesita pyenv)
# Simplemente instala múltiples SDKs — coexisten sin problema
dotnet --list-sdks # 6.0, 7.0, 8.0, 9.0
dotnet --list-runtimes
# El global.json en cada proyecto fija la versión
# {
# "sdk": { "version": "8.0.400" }
# }
Docker
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["MiApp/MiApp.csproj", "MiApp/"]
RUN dotnet restore "MiApp/MiApp.csproj"
COPY . .
WORKDIR "/src/MiApp"
RUN dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY --from=build /app .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MiApp.dll"]
Usa imágenes oficiales de mcr.microsoft.com/dotnet. dotnet/aspnet para runtime (~200 MB), dotnet/sdk para build (~800 MB). Multi-stage build reduce drásticamente el tamaño final.
Alias útiles
Añade a tu ~/.bashrc o ~/.zshrc:
alias dn='dotnet'
alias dnb='dotnet build'
alias dnr='dotnet run'
alias dnt='dotnet test'
alias dnp='dotnet publish'
alias dnn='dotnet new'
alias dnl='dotnet list'
alias dnw='dotnet watch'
Fuente: Microsoft .NET install docs, .NET Docker images
Escala de aprendizaje: de 0 a 100
Cada nivel incluye lo que debes saber y un proyecto para fijar los conceptos.
Nivel 0–15: Fundamentos absolutos
Qué aprender:
- Sintaxis básica:
Console.WriteLine(), variables, tipos (int,string,double,bool,decimal) - Control de flujo:
if/else if/else,switch(tradicional y expression),for,foreach,while,do-while - Pattern matching básico: type pattern, switch expression, property pattern
- Arrays y
List<T>: acceso por índice,Add(),Remove(),Count - Métodos:
static, parámetros,return, sobrecarga - Paradigmas: qué significa multiparadigma, diferencia entre imperativo, POO y funcional
- Conceptos clave: diferencia entre value type (
int,struct) y reference type (class,string).stringes inmutable — cada concatenación crea una nueva.varinfiere el tipo pero es tipado estático.usingpara recursos.
Proyecto: Calculadora CLI — suma, resta, multiplica, divide. Bucle while para repetir. Manejo de división por cero con try/catch. Menú con switch expression. Pattern matching para clasificar resultados. Muestra resultados con formato $"Resultado: {resultado:F2}".
Nivel 15–30: POO, tipos avanzados y colecciones
Qué aprender:
- Clases: constructores, fields, propiedades (
{ get; set; }), métodos - Encapsulación:
private,public,protected, propiedades con lógica - Namespaces,
using(global, static, alias), file-scoped namespaces enumpara constantes con nombrerecordpara datos inmutables (C# 9+)structpara tipos de valor pequeños- Tipos de datos: value types vs reference types, built-in (
int,long,float,double,decimal,char,bool), nullable value types (int?), nullable reference types (string?),var,dynamic,object string: inmutabilidad,Split(),Join(),Replace(),Contains(),StringBuilder- Arrays: multidimensional, jagged, covarianza
- Colecciones:
List<T>,Dictionary<K,V>,HashSet<T>,Queue<T>,Stack<T> DateTimeyTimeSpan- Conceptos clave:
readonlyvsconst. Boxing/unboxing — cuándo ocurre y cómo evitarlo. Stack vs heap. Propiedades auto-implementadas. Inicializadores de objeto.
Proyecto: Gestor de tareas CLI — clase Tarea con Id, Titulo, Completada, FechaCreacion. List<Tarea> como almacenamiento. Dictionary<string, Tarea> para búsquedas rápidas. Comandos: add, list, done, delete. Persistencia con System.Text.Json. Switch expression para procesar comandos. Nullable reference types para manejar valores opcionales.
Nivel 30–45: Herencia, interfaces, LINQ, archivos y algoritmos
Qué aprender:
- Herencia:
class Derivada : Base,base(),virtual/override,sealed - Clases abstractas vs interfaces: cuándo usar cada una
- Interfaces:
IComparable<T>,IDisposable,IEnumerable<T>, interfaces propias - LINQ: method syntax (
Where,Select,OrderBy,GroupBy,Join,Aggregate) y query syntax - Delegados:
Func<>,Action<>,Predicate<> - Lambdas:
(x, y) => x + y, lambdas estáticas y naturales - Sistema de archivos:
System.IO(File,FileInfo,Directory,Path),StreamReader/Writer,FileStream - Algoritmos:
Array.Sort(Introsort),List.Sort,BinarySearch,Comparer<T>,Comparison<T> - Big O de colecciones:
List<T>,Dictionary<K,V>,HashSet<T>,SortedDictionary<K,V>,SortedSet<T>,PriorityQueue<T> - Conceptos clave:
IEnumerable<T>es lazy — no se ejecuta hasta que iteres. LINQ Where/Select no modifican la colección original. Las interfaces permiten polimorfismo sin herencia.IDisposable+usingpara recursos no gestionados.
Proyecto: Sistema de inventario — clases Producto, Inventario con List<Producto>. Interfaz IAlmacenable. LINQ para búsquedas (Where, FirstOrDefault, OrderBy, GroupBy). IComparable<Producto> y Comparer<T> para ordenación. Serialización JSON a archivo con System.Text.Json. BinarySearch para búsqueda eficiente.
Nivel 45–60: Async, genéricos y testing
Qué aprender:
async/await:Task,Task<T>,WhenAll,WhenAnyHttpClientpara peticiones web- Genéricos: clases y métodos genéricos, constraints (
where T : class) - Pattern matching: switch expressions, property patterns, type patterns
- Nullable reference types:
string?,??,?.,??= - Testing: xUnit, NUnit, MSTest —
[Fact],[Theory],Assert - Conceptos clave: async es “I/O-bound” — no crea hilos nuevos, usa el threadpool eficientemente. Evita
.Resulty.Wait()(deadlock). Los nullable reference types son una feature de análisis en compilación, no en runtime. Pattern matching hace el código más legible que if/else anidados.
Proyecto: Cliente de clima API — llama a OpenWeatherMap o API similar con HttpClient. Deserializa JSON response con System.Text.Json. Tests unitarios con xUnit (usa ITestOutputHelper para debug). CancellationToken para timeout. Genéricos para un IRepositorio<T> base. Pattern matching para categorizar condiciones climáticas.
Nivel 60–75: ASP.NET Core, EF Core, archivos y serialización
Qué aprender:
- Minimal APIs (C# 6+):
app.MapGet/MapPost/MapPut/MapDelete - Inyección de dependencias (DI):
AddScoped,AddTransient,AddSingleton - Entity Framework Core: DbContext, DbSet, migraciones, LINQ queries
- Modelos con
required(C# 11),initproperties, data annotations - Middleware pipeline: logging, autenticación, CORS
- Configuración:
appsettings.json,IOptions<T>, environment variables - Sistema de archivos avanzado:
FileStreamasync,StreamReader/Writerasync,FileSystemWatcher - Serialización:
System.Text.Json(con Source generators), CSV con CsvHelper, YAML con YamlDotNet - Compresión:
System.IO.Compression(ZipArchive,GZipStream,DeflateStream) - Conceptos clave: DI es nativa en ASP.NET Core — no necesitas contenedor externo. EF Core usa change tracking —
AsNoTracking()para solo lectura. Las migraciones generan SQL, revísalo antes de aplicarlo en producción. Los middlewares son como tuberías (pipeline de request).
Proyecto: API de recetas — Minimal API con endpoints CRUD para recetas e ingredientes. EF Core + SQLite. Migraciones. DI para servicios y repositorios. Búsqueda por ingrediente con LINQ. Exportación a CSV con CsvHelper. Importación desde YAML con YamlDotNet. Compresión de imágenes con GZipStream.
Nivel 75–90: SignalR, Dapper, multimedia y producción
Qué aprender:
- SignalR: Hubs,
Clients.All, grupos, escalabilidad con Redis backplane - Dapper: queries SQL manuales,
Query<T>,Execute, multi-mapping - Docker: Dockerfile multi-stage, docker-compose con app + db + redis
- Caching:
IMemoryCache,IDistributedCachecon Redis - Background services:
BackgroundServicepara tareas programadas - Health checks:
AddHealthChecks()para monitoreo - OpenAPI/Swagger: documentación automática de endpoints
- Multimedia: SkiaSharp (gráficos 2D multiplataforma), ImageSharp (procesamiento de imágenes), OpenTK (OpenGL), NAudio (audio), OpenCVSharp (visión artificial)
- Conceptos clave: SignalR escala con un backplane (Redis, Azure SignalR Service). Dapper es 10-50x más rápido que EF Core para lecturas simples. SkiaSharp funciona en Windows, Linux, macOS y mobile. ImageSharp es puro .NET sin dependencias nativas.
BackgroundServicees la forma correcta de ejecutar tareas en background. Los health checks permiten a Kubernetes/orquestadores saber si la app está viva.
Proyecto: Dashboard de monitorización en tiempo real — API que recolecta métricas del sistema (CPU, RAM, disco). SignalR para streaming de métricas a clientes web. Dapper para almacenar histórico en PostgreSQL. BackgroundService que recolecta métricas cada 5 segundos. Docker Compose (api + postgres + redis). Health checks + Swagger. Generación de gráficos en tiempo real con SkiaSharp.
Nivel 90–100: Arquitectura y sistemas distribuidos
Qué aprender:
- Arquitectura limpia / hexagonal en .NET: separación en capas (Domain, Application, Infrastructure, API)
- gRPC: servicios con
protobuf, streaming bidireccional,Grpc.Net.Client - RabbitMQ / Azure Service Bus: mensajería entre microservicios
- Polly: resiliencia (retry, circuit breaker, timeout, bulkhead)
- OpenTelemetry: distributed tracing, métricas, logs
- Native AOT:
dotnet publish -aot, aplicaciones sin runtime - Source generators: metaprogramación en compilación
- Performance: BenchmarkDotNet para benchmarks, Memory diagnoser,
Span<T>yMemory<T>para evitar allocations - Conceptos clave: Native AOT produce binarios de ~2-3 MB que arrancan en milisegundos (ideal para serverless/containers).
Span<T>permite manipular memoria sin copias ni GC allocations. Polly con circuit breaker evita cascading failures. OpenTelemetry es el estándar para observabilidad.
Proyecto: Sistema de notificaciones distribuidas — microservicios con Clean Architecture. gRPC para comunicación interna. RabbitMQ como event bus. SignalR como BFF para clientes. Polly para resiliencia en llamadas externas. OpenTelemetry (Jaeger + Prometheus). Native AOT para el servicio de notificaciones. BenchmarkDotNet para asegurar rendimiento.
Fuente: ASP.NET Core docs, Clean Architecture (Steve Smith), gRPC for .NET, Polly docs
Primeros pasos y configuración del entorno
Si prefieres ver directamente el “Hola Mundo” en C#, consulta El Atlas del Hola Mundo.
Crear un proyecto nuevo
dotnet new console -o MiApp # App de consola
dotnet new webapi -o MiApi # ASP.NET Core Web API
dotnet new mvc -o MiMvc # ASP.NET Core MVC
dotnet new sln # Solución (SLN) para múltiples proyectos
Program.cs con top-level statements (C# 9+)
// Program.cs — ya no necesitas class Program ni Main
using System;
using System.Linq;
string nombre = "Mundo";
Console.WriteLine($"Hola {nombre}!");
int[] numeros = [1, 2, 3, 4, 5];
var pares = numeros.Where(n => n % 2 == 0);
Console.WriteLine(string.Join(", ", pares));
Estructura de proyecto (csproj)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.28" />
</ItemGroup>
</Project>
Nullable: activa nullable reference typesImplicitUsings:using System;,using System.Linq;, etc. automáticosPackageReference: paquetes NuGet
NuGet — gestión de paquetes
dotnet add package Dapper
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Newtonsoft.Json
dotnet list package # ver paquetes instalados
dotnet remove package Dapper
Editores recomendados
Los IDEs y editores más usados son VSCode, JetBrains y Neovim. Extensiones y configuraciones recomendadas.
Configuración inicial del proyecto
dotnet new console -o MiApp --use-program-main
cd MiApp
dotnet add package FluentAssertions
dotnet new tool-manifest
dotnet tool install dotnet-format
Archivo Directory.Build.props (configuración compartida entre proyectos):
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
Fuente: .NET CLI docs, NuGet docs
Paradigmas de programación
C# es multiparadigma: puedes escribir POO, funcional, imperativo, async y reactivo, a menudo combinándolos en un mismo proyecto.
POO puro
En C# todo es un objeto. Hasta int tiene métodos:
int n = 42;
Console.WriteLine(n.ToString("X")); // "2A"
Console.WriteLine(n.GetHashCode()); // 42
Toda clase deriva de object. Los value types heredan de ValueType → object. La herencia es simple (una clase base), pero puedes implementar múltiples interfaces.
Funcional
LINQ trajo map/filter/reduce, inmutabilidad con record, funciones de primera clase y pattern matching:
// LINQ = composición de funciones
var nums = Enumerable.Range(1, 100);
var resultado = nums
.Where(n => n % 2 == 0)
.Select(n => n * n)
.Aggregate((a, b) => a + b);
// Tuples con nombre
(int Suma, int Prod) Op(int a, int b) => (a + b, a * b);
var r = Op(3, 4);
Console.WriteLine($"{r.Suma}, {r.Prod}"); // 7, 12
// Pattern matching como expresión
string Clasificar(int edad) => edad switch
{
< 13 => "Niño",
>= 13 and < 20 => "Adolescente",
_ => "Adulto"
};
// Record: inmutabilidad e igualdad estructural
public record Persona(string Nombre, int Edad);
var p1 = new Persona("Ana", 30);
var p2 = p1 with { Edad = 31 }; // Copia inmutable, no muta p1
Imperativo
Bucles, statements y mutación explícita cuando el problema lo requiere:
int suma = 0;
for (int i = 0; i <= 100; i++)
if (i % 2 == 0) suma += i;
Async-first
C# fue pionero en hacer async parte del lenguaje con async/await. Toda API moderna de .NET expone versiones async:
string data = await File.ReadAllTextAsync("file.txt");
var json = await httpClient.GetStringAsync(url);
var users = await db.Usuarios.ToListAsync();
No hay threads bloqueados durante la espera — el compilador transforma el método en una máquina de estados.
Reactivo (Rx.NET / System.Reactive)
System.Reactive implementa el patrón Observer con streams y LINQ-style queries sobre eventos:
dotnet add package System.Reactive
using System.Reactive.Linq;
var ticks = Observable.Interval(TimeSpan.FromSeconds(1))
.Where(t => t % 2 == 0)
.Subscribe(t => Console.WriteLine($"Tick {t}"));
// Combinar streams
var clicks = Observable.FromEventPattern<EventArgs>(btn, "Click");
var moves = Observable.FromEventPattern<EventArgs>(btn, "MouseMove");
var merged = clicks.Merge(moves);
Útil para UI, tiempo real y pipelines de datos asíncronos.
Fuente: Functional C#, Rx.NET, ReactiveX
Tipos de datos y variables
Sistema de tipos
¿Qué es?
C# tiene tipado estático fuerte: el tipo de cada variable se conoce en tiempo de compilación y no se realizan conversiones implícitas entre tipos incompatibles. Es como una caja etiquetada: sabes exactamente qué contiene sin necesidad de abrirla.
El sistema de tipos es nominal (el nombre del tipo determina la compatibilidad), a diferencia del duck typing de Python o el structural typing de TypeScript.
🔗 Wikipedia — Tipado estático, Microsoft — C# type system
Sintaxis básica
int x = 42; // Tipado estático: x siempre es int
// x = "hola"; // ❌ Error de compilación
var y = 42; // Inferencia: y es int
dynamic z = 42; // Dynamic: tipo resuelto en runtime
z = "texto"; // OK — dynamic permite cambio de tipo
🧪 Cómo probarlo
// En xUnit
[Fact]
public void Tipado_estatico_no_permite_cambio()
{
int x = 42;
Assert.Equal(42, x);
// x = "texto"; // No compila
}
[Fact]
public void Dynamic_permite_cambio()
{
dynamic d = 42;
Assert.Equal(42, d);
d = "texto";
Assert.Equal("texto", d);
}
💡 Memoria y rendimiento
- Tipado estático: el compilador genera el código máquina exacto para cada tipo — sin overhead en runtime
vares azúcar sintáctico: no afecta al rendimiento, es idéntico a declarar el tipo explícitodynamicusa DLR (Dynamic Language Runtime) — añade overhead de resolución en runtimeobjectrequiere boxing/unboxing para value types
✅ Buenas prácticas
- ✅ Usa
varcuando el tipo sea obvio por la derecha (var dict = new Dictionary<string, int>()) - ❌ No abuses de
dynamic— pierdes seguridad de tipos, IntelliSense y rendimiento - ❌ No uses
varcuando oscurece la legibilidad (var x = ObtenerValor())
🏗️ Metodología
- En APIs y interfaces públicas, prefiere tipos explícitos sobre
var - En código interno (métodos privados/locales),
varmejora la legibilidad dynamices útil para interop con COM, JSON dinámico o lenguajes dinámicos
🔗 Para saber más
Value types vs Reference types
¿Qué es?
C# divide todos los tipos en dos categorías fundamentales. Los value types (struct) almacenan el valor directamente en la pila (o inline en el heap). Los reference types (class) almacenan una referencia al objeto en el heap, gestionado por el garbage collector.
Analogía: un value type es como una hoja de papel con el número escrito; un reference type es como la dirección de una casa — todos los que tienen la dirección ven la misma casa.
🔗 Microsoft — Value types, Reference types
Sintaxis básica
| Aspecto | Value type (struct) | Reference type (class) |
|---|---|---|
| Memoria | Stack o inline en heap | Heap (GC gestiona) |
| Asignación | int a = 5; | var obj = new Clase(); |
| Copia | Copia el valor completo | Copia la referencia |
null | No por defecto (int? sí) | Sí |
| Rendimiento | Menos presión GC | Más overhead |
struct Punto { public int X, Y; }
var p1 = new Punto { X = 1, Y = 2 };
var p2 = p1; p2.X = 99;
Console.WriteLine(p1.X); // 1 — copia independiente
class Persona { public string? Nombre; }
var a = new Persona { Nombre = "Ana" };
var b = a; b.Nombre = "Juan";
Console.WriteLine(a.Nombre); // "Juan" — misma referencia
🧪 Cómo probarlo
[Fact]
public void Value_type_copia_independiente()
{
var p1 = new Punto { X = 1, Y = 2 };
var p2 = p1;
p2.X = 99;
Assert.Equal(1, p1.X); // No se afecta
}
[Fact]
public void Reference_type_comparte_objeto()
{
var a = new Persona { Nombre = "Ana" };
var b = a;
b.Nombre = "Juan";
Assert.Equal("Juan", a.Nombre); // Mismo objeto
}
💡 Memoria y rendimiento
- Value types en stack: asignación y liberación inmediatas (push/pop)
- Reference types en heap: el GC debe recolectarlos — más presión de memoria
- Pasar un struct grande por valor COPIA todo — usar
refoinpara evitar copia - Boxing: un value type convertido a
objectse COPIA al heap — evítalo en bucles
✅ Buenas prácticas
- ✅ Usa
structpara datos pequeños (< 16 bytes) y lógica simple - ✅ Usa
readonly structpara garantizar inmutabilidad - ⚠️ No pases structs grandes por valor — usa
refoin - ❌ No hagas structs mutables — causa confusión (las copias no se sincronizan)
🏗️ Metodología
- Value types: números, coordenadas, flags, datos inmutables pequeños
- Reference types: objetos de dominio, servicios, datos que comparten estado
- readonly struct: cuando necesitas rendimiento de value type con seguridad de inmutabilidad
🔗 Para saber más
Built-in types (tipos primitivos)
¿Qué es?
C# proporciona un conjunto de tipos primitivos que son alias de tipos del sistema (System.Int32, System.Double, etc.). Cada uno tiene un tamaño fijo en memoria y un rango específico.
Sintaxis básica
| Tipo | Alias | Tamaño | Rango |
|---|---|---|---|
byte | System.Byte | 8 bits | 0–255 |
sbyte | System.SByte | 8 bits | -128 a 127 |
short | System.Int16 | 16 bits | -32,768 a 32,767 |
int | System.Int32 | 32 bits | ±2.1e9 |
long | System.Int64 | 64 bits | ±9.2e18 |
float | System.Single | 32 bits | ±3.4e38 |
double | System.Double | 64 bits | ±1.7e308 |
decimal | System.Decimal | 128 bits | ±7.9e28 (preciso) |
char | System.Char | 16 bits | Unicode UTF-16 |
bool | System.Boolean | 8 bits | true/false |
nint | System.IntPtr | 32/64 bits | Según plataforma |
int entero = 42;
long largo = 42L;
float flotante = 3.14f; // Sufijo f
double doble = 3.14; // Por defecto
decimal dinero = 19.99m; // Sufijo m — preciso para dinero
char letra = 'A';
bool activo = true;
🧪 Cómo probarlo
[Theory]
[InlineData(42, typeof(int))]
[InlineData(3.14, typeof(double))]
[InlineData(19.99m, typeof(decimal))]
public void Tipos_primitivos_corresponden(dynamic valor, Type tipoEsperado)
{
Assert.Equal(tipoEsperado, valor.GetType());
}
💡 Memoria y rendimiento
decimales más lento quedouble(~20x) — usadoublepara cálculos científicosfloates más rápido quedoubleen GPUs pero menos precisonintse optimiza según plataforma (32/64 bits) — útil para punteros y tamañosboolen realidad ocupa 1 byte (no 1 bit) en la mayoría de contextos
✅ Buenas prácticas
- ✅ Usa
decimalpara dinero y valores financieros - ✅ Usa
doublepara cálculos científicos y mediciones - ⚠️ No compares floats con
==— usaMath.Abs(a - b) < epsilon - ❌ No uses
floatpara dinero — la falta de precisión causa errores acumulativos
🏗️ Metodología
- En aplicaciones financieras:
decimalpara todos los valores monetarios - En juegos/gráficos:
float(las GPUs trabajan en float32) - En ciencia/datos:
double(precisión suficiente, buen rendimiento) - En sistemas embebidos: considera
byte/shortpara ahorrar memoria
🔗 Para saber más
- Microsoft — Floating-point numeric types
- What Every Computer Scientist Should Know About Floating-Point
Nullable types
¿Qué es?
C# tiene dos sistemas de nulabilidad: nullable value types (int?, bool?) desde C# 2, y nullable reference types (string?) desde C# 8. Los primeros son tipos especiales Nullable<T> que envuelven un value type. Los segundos son una característica de análisis de flujo en compilación.
🔗 Microsoft — Nullable value types, Nullable reference types
Sintaxis básica
// Nullable value types
int? edad = null;
edad = 25;
if (edad.HasValue)
Console.WriteLine(edad.Value);
int definitivo = edad ?? 0; // Null-coalescing
// Nullable reference types (C# 8+)
#nullable enable
string nombre = "Ana"; // No nullable — no puede ser null
string? apodo = null; // Nullable — puede ser null
Console.WriteLine(apodo?.Length); // Safe navigation: null si apodo es null
// Operadores null-condicionales
string? texto = ObtenerTexto();
int? longitud = texto?.Length; // null si texto es null
string saludo = texto ?? "default"; // Null-coalescing
texto ??= "valor por defecto"; // Null-coalescing assignment
🧪 Cómo probarlo
[Fact]
public void Nullable_value_type_funciona()
{
int? edad = null;
Assert.False(edad.HasValue);
edad = 25;
Assert.True(edad.HasValue);
Assert.Equal(25, edad.Value);
}
[Fact]
public void Null_coalescing_devuelve_default()
{
string? texto = null;
string resultado = texto ?? "default";
Assert.Equal("default", resultado);
}
💡 Memoria y rendimiento
int?esNullable<int>— ocupa ~8 bytes (4 del int + 4 del bool HasValue)- Nullable reference types son solo análisis en compilación — sin impacto en runtime
?.(null-conditional) genera una rama condicional en IL??(null-coalescing) genera una comparación y salto
✅ Buenas prácticas
- ✅ Habilita
Nullableen todos los proyectos nuevos (<Nullable>enable</Nullable>) - ✅ Usa
??sobre operador ternario cuando sea posible - ⚠️ No uses
!(null-forgiving operator) a menos que estés seguro — silencia warnings reales - ❌ No ignores los warnings de nullabilidad — son bugs potenciales
🏗️ Metodología
- Public APIs: prefiere no retornar null — usa
Option<T>o patternResult<T> - Bases de datos: usa
??para valores por defecto cuando leas columnas nullables - Deserialización JSON: los campos opcionales deben ser
string?, los requeridosstring
🔗 Para saber más
string e inmutabilidad
¿Qué es?
string en C# es una secuencia inmutable de caracteres Unicode (UTF-16). Cada operación que parece modificar un string crea uno nuevo en el heap. Es un reference type, pero se comporta como value type en cuanto a semántica de copia (inmutable).
Sintaxis básica
string s = "Hola";
string t = s;
s += " Mundo"; // Crea NUEVA cadena
Console.WriteLine(t); // "Hola" — inmutable
// Concatenación eficiente
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
sb.Append(i);
string resultado = sb.ToString();
// Raw string literals (C# 11)
string json = """
{
"nombre": "Ana",
"edad": 30
}
""";
// Interpolación
int edad = 30;
string msg = $"Tiene {edad} años";
🧪 Cómo probarlo
[Fact]
public void String_es_inmutable()
{
string s = "Hola";
string t = s;
s += " Mundo";
Assert.Equal("Hola", t); // t no cambia
}
[Fact]
public void StringBuilder_es_eficiente()
{
var sb = new StringBuilder();
for (int i = 0; i < 100; i++)
sb.Append(i);
Assert.Equal(100, sb.Length);
}
💡 Memoria y rendimiento
- Concatenación con
+en bucles es O(n²) — crea n cadenas intermedias StringBuilderes O(n) — reusa un buffer interno que crece al doble- String interning: las cadenas literales iguales comparten la misma memoria
string.IsNullOrEmpty()vsstr == ""— prefiere el método estático
✅ Buenas prácticas
- ✅ Prefiere
$""(interpolación) sobrestring.Format()o concatenación con+ - ✅ Usa
StringBuilderpara más de 3-4 concatenaciones o en bucles - ⚠️ No uses
+dentro de bucles con strings - ❌ No uses
string.Compare()cuando puedas usar==(salvo que necesites orden cultural)
🏗️ Metodología
- Para manipulación intensiva de texto:
StringBuilderoSpan<char> - Para parsing:
ReadOnlySpan<char>evita allocaciones - Para datos internacionales: usa
StringComparison.InvariantCultureoOrdinal
🔗 Para saber más
Colecciones
¿Qué es?
Las colecciones en C# son contenedores tipados para almacenar y manipular grupos de objetos. La mayoría viven en System.Collections.Generic y ofrecen diferentes garantías de rendimiento y ordenación.
Sintaxis básica
// List<T> — array dinámico
var lista = new List<int> { 1, 2, 3 };
lista.Add(4); lista.RemoveAt(0);
// Dictionary<K,V> — tabla hash O(1)
var dict = new Dictionary<string, int>
{
["Ana"] = 30, ["Juan"] = 25
};
dict.TryGetValue("Ana", out int edad);
// HashSet<T> — conjunto único O(1)
var set = new HashSet<int> { 1, 2, 3, 3, 3 };
Console.WriteLine(set.Count); // 3
// Queue<T> — FIFO
var q = new Queue<string>();
q.Enqueue("primero"); q.Enqueue("segundo");
Console.WriteLine(q.Dequeue()); // "primero"
// Stack<T> — LIFO
var p = new Stack<int>();
p.Push(1); p.Push(2);
Console.WriteLine(p.Pop()); // 2
// Collection expressions (C# 12)
List<int> nums = [1, 2, 3];
🧪 Cómo probarlo
[Fact]
public void HashSet_elimina_duplicados()
{
var set = new HashSet<int> { 1, 2, 2, 3 };
Assert.Equal(3, set.Count);
}
[Fact]
public void Dictionary_contiene_clave()
{
var dict = new Dictionary<string, int> { ["Ana"] = 30 };
Assert.True(dict.ContainsKey("Ana"));
Assert.False(dict.ContainsKey("Luis"));
}
[Fact]
public void Queue_es_FIFO()
{
var q = new Queue<string>();
q.Enqueue("primero"); q.Enqueue("segundo");
Assert.Equal("primero", q.Dequeue());
Assert.Equal("segundo", q.Dequeue());
}
💡 Memoria y rendimiento
| Colección | Acceso | Inserción | Búsqueda | Eliminación |
|---|---|---|---|---|
List<T> | O(1) | O(1)* | O(n) | O(n) |
Dictionary<K,V> | O(1) | O(1) | O(1) | O(1) |
HashSet<T> | - | O(1) | O(1) | O(1) |
Queue<T> | O(1) peek | O(1) | - | O(1) |
Stack<T> | O(1) peek | O(1) | - | O(1) |
*O(n) cuando el array interno necesita redimensionarse.
✅ Buenas prácticas
- ✅ Prefiere
List<T>sobreArrayList(sin boxing) - ✅ Usa
TryGetValuesobreContainsKey+[]en Dictionary (mitad de búsquedas) - ⚠️ No modifiques una colección mientras la iteras con
foreach - ❌ No asumas que
Dictionarymantiene orden de inserción (usaOrderedDictionary)
🏗️ Metodología
- List<T>: colección ordenada, acceso por índice, iteración frecuente
- Dictionary<K,V>: búsquedas por clave, cachés, índices secundarios
- HashSet<T>: membresía, duplicados, operaciones de conjunto
- Queue<T>: cola de procesamiento, BFS, buffers FIFO
- Stack<T>: backtracking, undo/redo, DFS
🔗 Para saber más
Arrays y Span<T>
¿Qué es?
Los arrays en C# son colecciones de tamaño fijo, contiguas en memoria. Span<T> (C# 7.2+) es una ventana sobre memoria contigua — puede apuntar a arrays, memoria nativa o stack — sin copiar datos.
Sintaxis básica
// Arrays — tamaño fijo
int[] arr = [1, 2, 3, 4, 5];
int[,] mat = { { 1, 2 }, { 3, 4 } };
int[][] jagged = [[1, 2], [3, 4, 5], [6]];
// Span<T> — slice sin copia
Span<int> slice = arr.AsSpan(2, 3);
slice[0] = 99;
Console.WriteLine(arr[2]); // 99 — el array original cambia
// stackalloc — buffer en stack, cero allocations
Span<byte> buffer = stackalloc byte[256];
// Memory<T> — versión heap-safe (para campos async)
Memory<int> mem = arr.AsMemory(2, 3);
🧪 Cómo probarlo
[Fact]
public void Span_modifica_array_original()
{
int[] arr = [1, 2, 3, 4, 5];
Span<int> slice = arr.AsSpan(1, 3);
slice[0] = 99;
Assert.Equal(99, arr[1]);
}
[Fact]
public void Array_covarianza_solo_reference()
{
string[] strings = ["a", "b"];
object[] objects = strings; // OK
Assert.Throws<ArrayTypeMismatchException>(() =>
{
objects[0] = 42; // Runtime error
});
}
💡 Memoria y rendimiento
- Los arrays son el tipo de colección más eficiente en memoria (solo los datos)
Span<T>elimina copias de memoria — operaciones sobre subconjuntos sin allocationsstackallocasigna en stack — ultrafino pero limitado (~1 MB de stack)- Los arrays multidimensionales
[,]son un solo bloque; los jagged[][]son arrays de arrays
✅ Buenas prácticas
- ✅ Usa
Span<T>para operaciones de slicing sin copia (procesamiento de texto, buffers) - ✅ Prefiere
stackallocpara buffers temporales pequeños - ⚠️ Los arrays multidimensionales pueden ser más lentos que jagged arrays
- ❌ No uses
Array.CopycuandoSpan<T>pueda hacer slicing
🏗️ Metodología
- Arrays: datos de tamaño fijo conocido en tiempo de compilación
Span<T>: procesamiento de alto rendimiento (parsers, serializadores)Memory<T>: operaciones async que necesitan buffer temporal
🔗 Para saber más
Control de flujo y modularidad
Control de flujo
¿Qué es?
El control de flujo determina el orden en que se ejecutan las instrucciones. C# ofrece condicionales, bucles, saltos y manejo de recursos. Su sintaxis deriva de C, con importantes mejoras como switch expressions, pattern matching y using declarations.
Sintaxis básica
if/else y switch:
int edad = 18;
if (edad >= 18)
Console.WriteLine("Mayor");
else if (edad >= 13)
Console.WriteLine("Adolescente");
else
Console.WriteLine("Niño");
// Switch expression (C# 8+)
string tipo = edad switch
{
< 13 => "Niño",
>= 13 and < 20 => "Adolescente",
_ => "Adulto"
};
// Pattern matching
object valor = 42;
string desc = valor switch
{
int i => $"Entero: {i}",
string s => $"Texto: {s}",
null => "Nulo",
_ => "Otro"
};
// List pattern (C# 11)
int[] nums = [1, 2, 3, 4, 5];
var dl = nums switch
{
[1, 2, .. var resto] => $"Empieza con 1,2, luego {resto.Length} más",
[] => "Vacía"
};
Bucles:
for (int i = 0; i < 10; i++) Console.WriteLine(i);
foreach (var n in Enumerable.Range(1, 10)) Console.WriteLine(n);
int j = 0;
while (j < 10) { Console.WriteLine(j); j++; }
int k = 0;
do { Console.WriteLine(k); k++; } while (k < 10);
break, continue, goto:
for (int i = 0; i < 10; i++)
{
if (i == 3) continue;
if (i == 7) break;
}
**using y checked/unchecked:**
```csharp
// using declaration — se descarta al salir del ámbito
using var reader = new StreamReader("archivo.txt");
int max = int.MaxValue;
checked { int r = max + 1; } // OverflowException
unchecked { int r = max + 1; } // Silencia overflow
🧪 Cómo probarlo
[Theory]
[InlineData(10, "Niño")]
[InlineData(15, "Adolescente")]
[InlineData(25, "Adulto")]
public void Switch_expression_clasifica_edad(int edad, string esperado)
{
string tipo = edad switch
{
< 13 => "Niño",
>= 13 and < 20 => "Adolescente",
_ => "Adulto"
};
Assert.Equal(esperado, tipo);
}
[Fact]
public void Pattern_matching_detecta_tipo()
{
object valor = 42;
string desc = valor switch
{
int i => $"Entero: {i}",
_ => "Otro"
};
Assert.Equal("Entero: 42", desc);
}
💡 Memoria y rendimiento
- Switch expression compila a tablas de salto (jump tables) cuando es posible — O(1)
- Pattern matching con múltiples casos es optimizado por el compilador
foreachsobre arrays usaGetEnumerator()— sin allocations para arraysgotoraramente necesario — suele indicar diseño mejorable
✅ Buenas prácticas
- ✅ Prefiere switch expression sobre if/else encadenados para múltiples condiciones
- ✅ Usa pattern matching para reducir verbose if/else + cast
- ⚠️ No anides switch dentro de switch — refactoriza a método separado
- ❌ No uses
gotoexcepto para salir de bucles anidados (raro)
🏗️ Metodología
- Condicionales simples (≤3 ramas): if/else es más legible
- Múltiples condiciones (≥4 ramas): switch expression
- Type checking: pattern matching con switch
- Bucles:
forcuando sabes el número de iteraciones;whilecuando no;foreachpara colecciones
🔗 Para saber más
Modularidad y organización del código
¿Qué es?
C# organiza el código en namespaces (contenedores lógicos), assemblies (unidades de compilación) y archivos físicos. Los proyectos .NET pueden tener múltiples assemblies que se referencian entre sí.
Sintaxis básica
Namespaces y usings:
namespace MiApp.Modelos;
using System;
using Proj = MiApp.Models; // Alias
global using System.Linq; // C# 10+: aplica a todo el proyecto
File-scoped namespaces (C# 10+):
namespace MiApp.Modelos;
public class Usuario { }
Assemblies:
dotnet new classlib -o MiLib
dotnet add MiApp reference MiLib
Partial class/struct:
// Archivo 1: Usuario.Part1.cs
public partial class Usuario
{
public string Nombre { get; set; }
}
// Archivo 2: Usuario.Part2.cs
public partial class Usuario
{
public override string ToString() => $"{Nombre}";
}
🧪 Cómo probarlo
// Verificar que un tipo es accesible desde otro assembly
[Fact]
public void Tipo_publico_es_accesible()
{
var type = typeof(MiLib.SomeClass);
Assert.NotNull(type);
}
💡 Memoria y rendimiento
- Los assemblies se cargan en memoria al primer uso (lazy loading)
global usingno afecta al rendimiento — es azúcar de compilaciónpartial classno tiene overhead en runtime — se fusiona en compilación
✅ Buenas prácticas
- ✅ Un archivo por tipo (clase/struct/interface), nombrado como el tipo
- ✅ Usa file-scoped namespaces (menos indentación)
- ⚠️ No uses
global usingpara todo — solo para lo que se usa en >80% de archivos - ❌ No pongas múltiples clases públicas en el mismo archivo (salvo tipos pequeños)
🏗️ Metodología
- Proyectos pequeños (
<5archivos): namespace único o ninguno - Librerías: namespace =
Empresa.Tecnologia.Componente - Proyectos grandes: separa en assemblies diferentes por capa (API, Dominio, Infraestructura)
🔗 Para saber más
Conceptos clave explicados a fondo
CLR, IL, JIT y Garbage Collector
¿Qué es?
C# no se compila a código máquina directamente. El compilador Roslyn genera IL (Intermediate Language). En runtime, el CLR (Common Language Runtime) activa el compilador JIT (Just-In-Time) que traduce IL a código nativo. El GC (Garbage Collector) gestiona la memoria automáticamente.
Sintaxis básica
// Tu código C# → IL → JIT → código nativo
int suma = 2 + 2;
// IL: ldc.i4.2 / ldc.i4.2 / add / stloc.0
// x86_64: mov eax, 2 / add eax, 2 / mov [rbp-4], eax
// GC generacional: Gen 0 (objetos nuevos), Gen 1, Gen 2 (longevos)
void EjemploGC()
{
var temporal = new byte[1024]; // Gen 0
var cache = new byte[1024 * 1024]; // LOH (> 85KB)
}
JIT vs AOT:
| Estrategia | Compilación | Rendimiento | Deploy |
|---|---|---|---|
| JIT (default) | En runtime | Mejora con tiered compilation | Necesita runtime |
| ReadyToRun | Pre-compila parcial | Inicio más rápido | Necesita runtime |
| Native AOT | Todo nativo | Inicio instantáneo | Autocontenido |
🧪 Cómo probarlo
[Fact]
public void CLR_ejecuta_IL_correctamente()
{
int suma = 2 + 2;
Assert.Equal(4, suma);
}
[Fact]
public void LOH_se_usa_para_objetos_grandes()
{
var grande = new byte[100_000]; // > 85KB → LOH
Assert.Equal(100_000, grande.Length);
}
💡 Memoria y rendimiento
- GC generacional: Gen 0 se recolecta en ~ms, Gen 2 es costosa (pausa ~100ms)
- LOH (>85KB) no se compacta por defecto — puede fragmentarse
- Native AOT elimina JIT en runtime pero limita la generación de código dinámico
- Tiered compilation: los métodos empiezan con JIT rápido y se re-JITean con optimizaciones si son llamados frecuentemente
✅ Buenas prácticas
- ✅ Evita llamar
GC.Collect()en producción - ✅ Usa Native AOT para serverless y CLIs; JIT para apps que necesitan reflexión pesada
- ⚠️ No asumas que objetos en Gen 0 se recolectan inmediatamente
- ❌ No crees objetos temporales grandes (>85KB) en bucles — llenan LOH rápido
🏗️ Metodología
- Apps web: JIT — necesitas reflexión y generación dinámica
- CLIs/serverless: Native AOT — inicio rápido, menor footprint
- Juegos: ReadyToRun para balance entre inicio y rendimiento
🔗 Para saber más
Value types vs Reference types, Boxing/Unboxing
¿Qué es?
C# divide los tipos en dos categorías: value types (almacenados en stack o inline en heap) y reference types (siempre en heap, GC gestiona). Entender la diferencia es crucial para rendimiento y predictibilidad.
Sintaxis básica
// Value type: struct — se copia por valor
struct Punto { public int X { get; set; } public int Y { get; set; } }
var p1 = new Punto { X = 1, Y = 2 };
var p2 = p1;
p2.X = 99;
Console.WriteLine(p1.X); // 1 — no se afecta
// Reference type: class — se copia la referencia
var persona1 = new Persona { Nombre = "Ana" };
var persona2 = persona1;
persona2.Nombre = "Juan";
Console.WriteLine(persona1.Nombre); // "Juan" — mismo objeto
// Boxing: value type → object (crea objeto en heap)
int numero = 42;
object obj = numero; // Boxing
int numero2 = (int)obj; // Unboxing
// Genéricos evitan boxing
var listaBuena = new List<int>(); // Sin boxing
🧪 Cómo probarlo
[Fact]
public void Struct_se_copia_por_valor()
{
var p1 = new Punto { X = 1, Y = 2 };
var p2 = p1;
p2.X = 99;
Assert.Equal(1, p1.X); // p1 no cambia
}
[Fact]
public void List_generica_evita_boxing()
{
var list = new List<int> { 42 };
Assert.Equal(42, list[0]); // Sin boxing
}
💡 Memoria y rendimiento
- Value types en stack: sin presión de GC, más rápidos
- Cada boxing aloca en heap y copia — evítalo en bucles calientes
- Structs grandes (>16 bytes) pueden ser más lentos que classes por la copia completa
- Genéricos eliminan boxing — usa
List<T>nuncaArrayList
✅ Buenas prácticas
- ✅ Usa
structpara datos pequeños (<16bytes) e inmutables - ✅ Usa
classpara la mayoría de tipos (más flexibles, menos copias) - ⚠️ No pases structs grandes por parámetro sin
in(copia completa) - ❌ No uses colecciones no genéricas (
ArrayList,Hashtable) — siempre boxing
🏗️ Metodología
- Datos de valor:
structreadonly para coordenadas, vectores, colores - Entidades/DTOs:
classorecord(reference type) - Hot paths: prefiere structs pequeños para reducir presión de GC
- APIs: usa genéricos para evitar boxing en parámetros
🔗 Para saber más
Tipos primitivos, string y colecciones
¿Qué es?
Los tipos primitivos son alias de tipos System.* (int → Int32). string es inmutable. Las colecciones base incluyen arrays, List<T>, Dictionary<,>, HashSet<T>, Queue<T> y Stack<T>.
Sintaxis básica
// Primitivos
int entero = 42;
double pi = 3.1416;
decimal precio = 19.99m;
bool activo = true;
// string — inmutable
string s1 = "Hola";
s1 += " Mundo"; // Crea NUEVA cadena
// StringBuilder — para mutación eficiente
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++) sb.Append(i);
// Arrays
int[] arr = [1, 2, 3];
Span<int> slice = arr.AsSpan(1, 2); // Sin copia
// Colecciones genéricas
var lista = new List<int> { 1, 2, 3 };
var dict = new Dictionary<string, int> { ["Ana"] = 30 };
var hashSet = new HashSet<int> { 1, 2, 3 };
var cola = new Queue<string>();
var pila = new Stack<int>();
🧪 Cómo probarlo
[Fact]
public void StringBuilder_es_mas_eficiente_que_concatenacion()
{
var sb = new StringBuilder();
for (int i = 0; i < 100; i++) sb.Append(i);
Assert.Equal(100, sb.Length);
}
[Fact]
public void HashSet_elimina_duplicados()
{
var set = new HashSet<int> { 1, 1, 2, 3 };
Assert.Equal(3, set.Count);
}
💡 Memoria y rendimiento
stringinmutable: cada concatenación crea un nuevo objeto — O(n²) en buclesStringBuilderusa buffer interno que crece exponencialmente — O(n) en bucles- String interning: literales iguales comparten la misma memoria
- Colecciones genéricas no generan boxing; las no genéricas sí
✅ Buenas prácticas
- ✅ Usa
StringBuilderpara múltiples concatenaciones en bucles - ✅ Prefiere
List<T>sobre arrays cuando el tamaño puede cambiar - ⚠️ No modifiques cadenas en bucles grandes — usa
StringBuilder - ❌ No uses
ArrayList,Hashtable— siempre prefiere las versiones genéricas
🏗️ Metodología
- Texto dinámico:
StringBuilderpara logs, generación de HTML/CSV - Datos fijos: arrays (tamaño conocido) o
ReadOnlySpan<T>para máximo rendimiento - Lookups frecuentes:
Dictionary<K,V>para búsqueda por clave - Unicidad:
HashSet<T>para eliminar duplicados
🔗 Para saber más
POO: clases, interfaces, abstract, sealed, records y structs
¿Qué es?
C# es un lenguaje orientado a objetos con herencia simple de clases e implementación múltiple de interfaces. Soporta clases, structs, records (C# 9+), abstract y sealed para controlar la jerarquía.
Sintaxis básica
// Clase con herencia simple
public class Animal { public string Nombre { get; init; } public virtual string HacerSonido() => "..."; }
public class Perro : Animal { public override string HacerSonido() => "Guau"; }
// Interfaz — múltiples implementaciones
public interface IVolador { void Volar(); }
public class Pato : IVolador, INadador { public void Volar() { } public void Nadar() { } }
// Abstract — no instanciable, obliga a implementar
public abstract class Forma { public abstract double Area(); }
public sealed class Circulo : Forma { public double Radio { get; } public override double Area() => Math.PI * Radio * Radio; }
// Record — inmutable, equality por valor (C# 9+)
public record Persona(string Nombre, int Edad);
var p2 = new Persona("Ana", 30) with { Edad = 31 };
// Struct — value type, copia por valor
public readonly struct Coordenadas(double lat, double lon);
🧪 Cómo probarlo
[Fact]
public void Record_equality_por_valor()
{
var p1 = new Persona("Ana", 30);
var p2 = new Persona("Ana", 30);
Assert.Equal(p1, p2); // True — records comparan por valor
}
[Fact]
public void Clase_sealed_no_puede_heredarse()
{
var c = new Circulo(5);
Assert.Equal(Math.PI * 25, c.Area(), precision: 10);
}
💡 Memoria y rendimiento
recordgeneraEquals/GetHashCode/ToStringautomáticamente — sin reflexión en runtimesealeden métodos permite devirtualización (inline) por el JITstructno genera presión de GC pero se copia al pasar como parámetroabstractno tiene overhead directo — solo marca métodos como virtual
✅ Buenas prácticas
- ✅ Usa
recordpara DTOs, eventos, y datos inmutables - ✅ Usa
sealeden clases que no deben heredarse — mejora rendimiento - ⚠️ No crees jerarquías profundas (>3 niveles) — prefiere composición
- ❌ No uses
structpara objetos >16 bytes — la copia es cara
🏗️ Metodología
- Modelos de datos:
record(inmutable, value equality) - Servicios:
classcon interfaces (dependencias inyectables) - Configuración:
recordconinitproperties - Hot paths:
readonly structpara datos pequeños
🔗 Para saber más
Propiedades y fields
¿Qué es?
Las propiedades encapsulan campos con getters/setters. C# ofrece auto-propiedades, init-only setters y required properties. Son la forma canónica de exponer datos en una clase.
Sintaxis básica
public class Producto
{
private decimal _precio; // Field privado
public string Nombre { get; set; } // Auto-propiedad
public string Codigo { get; } // Solo init en constructor
public DateTime FechaCreacion { get; init; } // Init-only (C# 9+)
public required string SKU { get; init; } // Required (C# 11)
public decimal PrecioConIVA => Precio * 1.21m; // Calculada
public decimal Precio
{
get => _precio;
set => _precio = value >= 0 ? value
: throw new ArgumentException("El precio no puede ser negativo");
}
}
🧪 Cómo probarlo
[Fact]
public void Propiedad_con_validacion_lanza_excepcion()
{
var p = new Producto { Nombre = "Test", Precio = 10 };
Assert.Throws<ArgumentException>(() => p.Precio = -1);
}
💡 Memoria y rendimiento
- Auto-propiedades se compilan a fields con get/set — sin overhead
- Init-only setters se verifican en compilación, no en runtime
requiredse valida en compilación con análisis de flujo
✅ Buenas prácticas
- ✅ Usa auto-propiedades para la mayoría de casos
- ✅ Prefiere
initsobresetpara inmutabilidad - ⚠️ No agregues lógica pesada en getters — los consumidores no lo esperan
- ❌ No expongas campos públicos — siempre usa propiedades
🏗️ Metodología
- DTOs: auto-propiedades con
init - Entidades: propiedades con lógica de validación
- Config: required properties para asegurar inicialización completa
Delegados, eventos y lambdas
¿Qué es?
Los delegados son types que definen firmas de métodos. Los eventos notifican cambios a suscriptores. Las lambdas son funciones anónimas. C# ofrece delegados predefinidos como Func<>, Action<> y Predicate<>.
Sintaxis básica
// Delegado y lambdas
Func<int, int, int> sumar = (a, b) => a + b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> esPar = x => x % 2 == 0;
// Evento
public class Sensor
{
public event EventHandler<double>? TemperaturaExcedida;
private double _temperatura;
public double Temperatura
{
get => _temperatura;
set { _temperatura = value; if (value > 50) TemperaturaExcedida?.Invoke(this, value); }
}
}
// Lambda static — no captura variables (C# 9)
Func<int, int, int> sumStatic = static (a, b) => a + b;
🧪 Cómo probarlo
[Fact]
public void Evento_se_dispara_cuando_se_supera_limite()
{
var sensor = new Sensor();
double temp = 0;
sensor.TemperaturaExcedida += (_, t) => temp = t;
sensor.Temperatura = 55;
Assert.Equal(55, temp);
}
[Fact]
public void Lambda_static_no_captura_variables()
{
Func<int, int> f = static x => x * x;
Assert.Equal(25, f(5));
}
💡 Memoria y rendimiento
- Delegados son reference types — alocan en heap
- Eventos son multicast delegates — mantienen lista de invocación
- Lambdas que capturan variables locales alocan un closure object
- Lambda
static(C# 9) evita closure allocation
✅ Buenas prácticas
- ✅ Usa
EventHandler<TEventArgs>en vez de delegados personalizados para eventos - ✅ Prefiere lambda
staticcuando no captures variables - ⚠️ No suscribas eventos sin desuscribir — memory leaks
- ❌ No pases lambdas como
Expression<>si solo necesitas delegados
🏗️ Metodología
- Notificaciones: eventos con
EventHandler<T> - Callbacks:
Func<>/Action<>como parámetros - Filtros:
Predicate<T>oFunc<T, bool> - Hot paths: lambda static para evitar allocations repetidos
LINQ (Language Integrated Query)
¿Qué es?
LINQ permite consultar colecciones, bases de datos y XML con sintaxis integrada en C#. Soporta method syntax (fluent) y query syntax (SQL-like). Todas las operaciones son lazy evaluation hasta que se materializan con ToList()/ToArray().
Sintaxis básica
var nums = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Method syntax (fluent)
var result = nums.Where(n => n % 2 == 0).Select(n => n * 10).OrderByDescending(n => n).Take(3).ToList();
// Query syntax (SQL-like)
var result2 = from n in nums where n % 2 == 0 orderby n descending select n * 10;
// Operadores clave
.Where(n => n > 5).Select(n => new { Valor = n, Cuadrado = n * n })
.Skip(5).Take(5).Chunk(3)
.OrderBy(p => p.Nombre).ThenByDescending(p => p.Edad)
.Count().Sum(p => p.Precio).Average().Aggregate((a, b) => a + b)
.Any(p => p.Edad > 18).All(p => p.Activo).Contains(42)
.Distinct().Union(otra).Intersect(otra).Except(otra)
.GroupBy(p => p.Categoria).Select(g => new { Categoria = g.Key, Total = g.Sum(p => p.Precio) })
// Lazy evaluation
var query = nums.Where(n => n > 3); // NO se ejecuta aquí
var list = query.ToList(); // Aquí se materializa
🧪 Cómo probarlo
[Fact]
public void LINQ_filtra_y_ordena()
{
int[] nums = [3, 1, 4, 1, 5];
var result = nums.Where(n => n > 2).OrderByDescending(n => n).ToList();
Assert.Equal([5, 4, 3], result);
}
💡 Memoria y rendimiento
- Lazy evaluation evita crear colecciones intermedias innecesarias
- Cada materialización re-ejecuta la query — llama a
ToList()una vez y cachea Selectno aloca hasta materializar- Operaciones como
Count()sin filtro son O(1) paraList<T>/arrays
✅ Buenas prácticas
- ✅ Materializa con
ToList()/ToArray()si vas a iterar múltiples veces - ✅ Usa
Any()en vez deCount() > 0para verificar existencia - ⚠️ No abuses de LINQ en hot paths — foreach explícito es más rápido
- ❌ No llames
ToList()antes de filtrar — rompe lazy evaluation
Genéricos, constraints, covarianza/contravarianza
¿Qué es?
Los genéricos permiten definir tipos y métodos parametrizados por tipo. Las constraints restringen qué tipos pueden usarse. Covarianza (out) permite usar tipos derivados; contravarianza (in) permite usar tipos base.
Sintaxis básica
// Clase genérica
public class Repositorio<T>
{
private readonly List<T> _items = [];
public void Agregar(T item) => _items.Add(item);
public T Obtener(int index) => _items[index];
}
// Constraints
// where T : class | struct | new() | BaseClass | IInterface | notnull | unmanaged | Enum | Delegate
public class Fabrica<T> where T : new()
{
public T Crear() => new T();
}
// Covarianza (out): IEnumerable<string> → IEnumerable<object>
IEnumerable<string> cadenas = ["hola"];
IEnumerable<object> objetos = cadenas;
// Contravarianza (in): Action<object> → Action<string>
Action<object> accionObj = obj => Console.WriteLine(obj);
Action<string> accionStr = accionObj;
🧪 Cómo probarlo
[Fact]
public void Repositorio_generico_funciona_con_cualquier_tipo()
{
var repo = new Repositorio<int>();
repo.Agregar(42);
Assert.Equal(42, repo.Obtener(0));
}
💡 Memoria y rendimiento
- Los genéricos se especializan por tipo en JIT — sin boxing ni casting
- Cada tipo cerrado (ej.
List<int>) genera código nativo separado structcomo genérico no aloca en heap si el contenedor es stack-only
✅ Buenas prácticas
- ✅ Usa genéricos para evitar duplicar código con diferentes tipos
- ✅ Documenta constraints con
wherecláusulas explícitas - ⚠️ No uses
objectcomo parámetro — prefiere genéricos con constraints - ❌ No especialices genéricos con demasiados tipos cerrados — crece el código nativo
async/await, Task, ValueTask, IAsyncEnumerable
¿Qué es?
async/await permite escribir código asíncrono que no bloquea hilos. Task representa una operación en curso. ValueTask es un struct que evita allocations para resultados síncronos. IAsyncEnumerable<T> permite streams asíncronos con await foreach.
Sintaxis básica
// async/await básico
public async Task<string> ObtenerDatosAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
// ValueTask — evita allocation si el resultado es síncrono
public ValueTask<int> ObtenerValorAsync(string key)
{
if (_cache.TryGetValue(key, out int valor))
return new ValueTask<int>(valor); // Sin allocation
return new ValueTask<int>(CalcularAsync(key));
}
// Múltiples tareas concurrentes
var tareas = Enumerable.Range(0, 10).Select(i => ObtenerDatosAsync($"https://api.example.com/{i}"));
string[] resultados = await Task.WhenAll(tareas);
// Cancelación
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(3));
await Task.Delay(5000, cts.Token); // Lanza OperationCanceledException
// IAsyncEnumerable<T> — streams asíncronos
public async IAsyncEnumerable<int> GenerarAsync()
{
for (int i = 0; i < 10; i++) { await Task.Delay(100); yield return i; }
}
await foreach (int num in GenerarAsync()) Console.WriteLine(num);
🧪 Cómo probarlo
[Fact]
public async Task Async_metodo_completa_correctamente()
{
string result = await ObtenerDatosAsync("https://example.com");
Assert.NotNull(result);
}
[Fact]
public async Task Cancelacion_lanza_excepcion()
{
var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(() =>
Task.Delay(5000, cts.Token));
}
💡 Memoria y rendimiento
async/awaitcompila a una máquina de estados (state machine) en struct — sin threadValueTaskevita allocation deTaskcuando el resultado es síncronoTask.WhenAlles más eficiente que esperar secuencialmente- Cada
awaitpuede devolver el hilo al pool si la operación no está completa
✅ Buenas prácticas
- ✅ Usa
*Asyncen toda I/O (red, disco, BD) — nunca bloquees con.Result - ✅ Pasa
CancellationTokena todos los métodos async - ⚠️ No hagas
Task.WhenAllcon miles de tareas — usaParallel.ForEachAsync - ❌ No uses
async voidexcepto en event handlers — las excepciones son inmanejables
Pattern matching
¿Qué es?
El pattern matching permite comprobar la forma de un valor (tipo, propiedades, posición) y extraer datos en una sola expresión. Evolucionó desde C# 7 con type patterns hasta C# 11 con list patterns.
🔗 Microsoft — Pattern matching
Sintaxis básica
object valor = 42;
// Type pattern
if (valor is int entero) Console.WriteLine(entero);
// Switch expression
string desc = valor switch
{
int i when i > 0 => $"Entero positivo: {i}",
string s => $"Cadena: {s}",
null => "Nulo",
_ => "Otro"
};
// Property pattern
if (valor is string { Length: > 5 } texto) Console.WriteLine($"Largo: {texto}");
// List patterns (C# 11)
int[] nums = [1, 2, 3, 4, 5];
var res = nums switch
{
[1, 2, .. var resto] => $"Empieza con 1,2, luego {resto.Length} más",
[var primero, .., var ultimo] => $"Primero: {primero}, último: {ultimo}",
[] => "Vacía"
};
// Relational patterns (C# 9+)
string Nota(int p) => p switch
{
>= 90 => "Sobresaliente",
>= 70 => "Notable",
>= 50 => "Aprobado",
< 50 => "Suspenso"
};
🧪 Cómo probarlo
[Theory]
[InlineData(95, "Sobresaliente")]
[InlineData(75, "Notable")]
[InlineData(55, "Aprobado")]
[InlineData(30, "Suspenso")]
public void Pattern_matching_en_notas(int puntuacion, string esperado)
{
string nota = puntuacion switch
{
>= 90 => "Sobresaliente",
>= 70 => "Notable",
>= 50 => "Aprobado",
< 50 => "Suspenso"
};
Assert.Equal(esperado, nota);
}
💡 Memoria y rendimiento
- Pattern matching compila a código eficiente — tablas de salto cuando es posible
- Switch expression con patrones es tan rápida como if/else manual
- List patterns tienen overhead mínimo — el compilador optimiza
✅ Buenas prácticas
- ✅ Usa pattern matching para simplificar if/else + cast verbose
- ✅ Prefiere switch expression para múltiples condiciones de tipo
- ⚠️ No hagas pattern matching en objetos grandes en hot paths
- ❌ No uses pattern matching como sustituto de polimorfismo bien diseñado
Nullable reference types
¿Qué es?
Desde C# 8, los tipos referencia pueden ser nullable (string?) o non-nullable (string). El compilador analiza el flujo para prevenir NullReferenceException en tiempo de compilación.
🔗 Microsoft — Nullable reference types
Sintaxis básica
#nullable enable
public class Usuario
{
public string Nombre { get; set; } // No nullable
public string? Email { get; set; } // Nullable
}
string? texto = ObtenerTexto();
int? longitud = texto?.Length; // ?. condicional
string saludo = texto ?? "default"; // ?? coalescing
texto ??= "valor"; // ??= asignación (C# 8+)
void Procesar(string? cadena)
{
if (cadena is null) return;
Console.WriteLine(cadena.Length); // Compilador sabe que no es null
}
// Null-forgiving operator
string nombre = ObtenerNombre()!; // "Confía en mí, no es null"
🧪 Cómo probarlo
[Fact]
public void Null_coalescing_devuelve_default_cuando_null()
{
string? texto = null;
Assert.Equal("default", texto ?? "default");
}
Extension methods y partial classes
¿Qué es?
Los extension methods añaden métodos a tipos existentes sin modificar el tipo original. Son métodos estáticos en clases estáticas con el primer parámetro marcado con this. Las partial classes dividen la definición de una clase en múltiples archivos.
🔗 Microsoft — Extension Methods
Sintaxis básica
// Extension method
public static class StringExtensions
{
public static bool EsPalindromo(this string texto) { /* ... */ }
public static string Truncar(this string texto, int maxLength) =>
texto?.Length > maxLength ? texto[..maxLength] + "..." : texto;
}
// Uso
"Anita lava la tina".EsPalindromo(); // True
// Partial class
// Archivo 1: Persona.Part1.cs
public partial class Persona { public string Nombre { get; set; } }
// Archivo 2: Persona.Part2.cs
public partial class Persona { public override string ToString() => Nombre; }
🧪 Cómo probarlo
[Fact]
public void Extension_method_funciona_en_null()
{
string? nula = null;
Assert.Null(nula.Truncar(5)); // Maneja null internamente
}
Top-level statements, global usings y file-scoped namespaces
¿Qué es?
C# 9+ introdujo top-level statements (programa sin class/Main), C# 10+ global usings (importan en todo el proyecto) y file-scoped namespaces (sin llaves). Simplifican drásticamente el boilerplate.
🔗 Microsoft — Top-level statements
Sintaxis básica
// Program.cs — sin class, sin Main
using System;
var numeros = new[] { 1, 2, 3 };
Console.WriteLine(numeros.Sum());
static string Saludar(string nombre) => $"Hola {nombre}";
// GlobalUsings.cs — aplica a TODO el proyecto
global using System;
global using System.Linq;
// File-scoped namespace (C# 10+)
namespace MiApp.Modelos;
public class Usuario { }
🧪 Cómo probarlo
// En proyectos de test, ImplicitUsings ya incluye System, LINQ, etc.
[Fact]
public void Top_level_statements_son_equivalentes()
{
// El comportamiento es idéntico a tener Program.Main
Assert.True(true);
}
Source generators (metaprogramación en compilación)
¿Qué es?
Los source generators (C# 9+) son piezas de código que se ejecutan durante la compilación para generar código fuente adicional. Son la alternativa moderna a T4 templates y Reflection pesada. El compilador Roslyn los invoca automáticamente.
🔗 Microsoft — Source Generators
Sintaxis básica
[Generator]
public class HelloGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
context.AddSource("HelloWorld.g.cs", """
namespace GeneratedCode;
public static class HelloWorld
{
public static string GetMessage() => "Generado en compilación!";
}
""");
}
}
// Source generators famosos
// System.Text.Json — serializadores sin reflexión
// Regex source generator (C# 11+)
public partial class EmailValidator
{
[GeneratedRegex(@"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")]
public static partial Regex EmailRegex();
}
🧪 Cómo probarlo
[Fact]
public void Source_generated_regex_es_valida()
{
Assert.True(EmailValidator.EmailRegex().IsMatch("test@example.com"));
Assert.False(EmailValidator.EmailRegex().IsMatch("invalido"));
}
💡 Memoria y rendimiento
- Source generators eliminan reflexión y compilación en runtime — ideales para NativeAOT
- El regex source generator compila el patrón en build time, no en runtime
- Cero overhead en runtime — el código generado es estático
✅ Buenas prácticas
- ✅ Usa
[GeneratedRegex]en vez denew Regex()para patrones fijos - ✅ Para serialización de alto rendimiento, usa
JsonSerializerContext - ⚠️ No generes código que podría escribirse fácilmente a mano
- ❌ No uses source generators para lógica de negocio — son para infraestructura
Sistema de archivos y serialización
File, Directory y Path
¿Qué es?
El espacio System.IO proporciona clases para operar sobre archivos y directorios. File y Directory ofrecen métodos estáticos para operaciones rápidas. FileInfo y DirectoryInfo son útiles para múltiples operaciones sobre el mismo recurso. Path manipula rutas de forma multiplataforma.
Sintaxis básica
using System.IO;
// File — estático
string contenido = File.ReadAllText("archivo.txt");
File.WriteAllText("salida.txt", "Hola Mundo");
File.Copy("origen.txt", "destino.txt", overwrite: true);
bool existe = File.Exists("archivo.txt");
// FileInfo — instancia
var fi = new FileInfo("archivo.txt");
Console.WriteLine($"{fi.Name}, {fi.Length} bytes");
// Directory — estático
string[] txts = Directory.GetFiles("./", "*.txt");
Directory.CreateDirectory("MisDatos");
// DirectoryInfo — instancia
var di = new DirectoryInfo("./");
foreach (var f in di.GetFiles("*.cs"))
Console.WriteLine(f.Name);
// Path — rutas multiplataforma
string combo = Path.Combine("carpeta", "sub", "archivo.txt");
string ext = Path.GetExtension("archivo.txt");
string tempFile = Path.GetTempFileName();
🧪 Cómo probarlo
[Fact]
public void File_Write_read_roundtrip()
{
var path = Path.GetTempFileName();
File.WriteAllText(path, "Hola pruebas");
var content = File.ReadAllText(path);
Assert.Equal("Hola pruebas", content);
File.Delete(path);
}
[Fact]
public void Path_combine_unixa_separator()
{
string result = Path.Combine("a", "b", "c.txt");
Assert.Equal("a/b/c.txt", result);
}
💡 Memoria y rendimiento
File.ReadAllTextcarga todo el archivo en memoria — no apto para archivos >100 MBFile.ReadLines(deferred) itera línea a línea sin cargar todoFileInfocachea metadatos en la primera llamada — usaRefresh()si cambia externamentePath.Combinees barato — usa el separador del SO automáticamente
✅ Buenas prácticas
- ✅ Usa
Path.Combineen vez de concatenar cadenas con"/"o"\\"manualmente - ✅ Prefiere
File.ReadLinespara archivos grandes en vez deReadAllText - ⚠️ No asumas rutas fijas — usa
Environment.GetFolderPathpara carpetas del sistema - ❌ No guardes archivos temporales sin limpiarlos — usa
Path.GetTempFileNamey elimínalos
🏗️ Metodología
- Configuración: almacena rutas en configuración (appsettings.json), nunca hardcodees
- Logs: escribe a
ApplicationDataoLogs/con rotación de archivos - Uploads: sanitiza nombres, limita tamaño, guarda fuera de
wwwroot - Pruebas: usa
Path.GetTempPath()para archivos temporales que se limpian después
🔗 Para saber más
Streams: lectura, escritura y async I/O
¿Qué es?
Un Stream es una abstracción de una secuencia de bytes. FileStream, MemoryStream y GZipStream son tipos comunes. Los streams soportan operaciones síncronas y asíncronas (ReadAsync/WriteAsync). StreamReader/StreamWriter añaden capa de texto.
Sintaxis básica
// StreamReader — texto línea a línea
using var reader = new StreamReader("archivo.txt");
string? linea;
while ((linea = await reader.ReadLineAsync()) != null)
Console.WriteLine(linea);
// StreamWriter — escritura de texto
using var writer = new StreamWriter("salida.txt", append: true);
await writer.WriteLineAsync("Nueva línea");
// FileStream — binario
using var fs = new FileStream("datos.bin", FileMode.OpenOrCreate);
byte[] buffer = new byte[4096];
int bytesRead = await fs.ReadAsync(buffer);
// BinaryReader/Writer — tipos primitivos
using var bw = new BinaryWriter(File.Open("datos.bin", FileMode.Create));
bw.Write(42); bw.Write(3.14); bw.Write("Hola");
// Async file helpers
await File.WriteAllTextAsync("archivo.txt", "contenido");
string texto = await File.ReadAllTextAsync("archivo.txt");
🧪 Cómo probarlo
[Fact]
public async Task Stream_escribe_y_lee_texto()
{
var path = Path.GetTempFileName();
await File.WriteAllTextAsync(path, "línea 1\nlínea 2");
var lines = await File.ReadAllLinesAsync(path);
Assert.Equal(2, lines.Length);
File.Delete(path);
}
[Fact]
public void MemoryStream_funciona_sin_archivo()
{
using var ms = new MemoryStream();
using var writer = new StreamWriter(ms);
writer.Write("en memoria");
writer.Flush();
Assert.True(ms.Length > 0);
}
💡 Memoria y rendimiento
- Operaciones async evitan bloquear el thread de UI — críticas en apps gráficas
FileStreamtiene buffer interno (4 KB por defecto) — ajusta conFileStreamOptionsMemoryStreamtrabaja en RAM — útil para tests y transformaciones en memoriaCopyToAsyncoptimiza la copia entre streams sin buffers intermedios grandes
✅ Buenas prácticas
- ✅ Siempre usa
usingoawait usingpara streams — liberan handles del SO - ✅ Usa métodos
*Asyncen servidores web y UI — evitas thread pool starvation - ⚠️ No mezcles
ReadyReadAsyncen el mismo stream — resultados impredecibles - ❌ No llames
Disposemanualmente si usasusing
🏗️ Metodología
- Archivos pequeños (
<10MB):ReadAllText/WriteAllText(simple) - Archivos grandes:
FileStreamcon buffer oReadLines(deferred) - Streams en red/API: siempre async para no bloquear
- Transformaciones: encadena streams (
GZipStreamsobreFileStream)
🔗 Para saber más
FileSystemWatcher y compresión
¿Qué es?
FileSystemWatcher monitoriza cambios en archivos/directorios y dispara eventos. System.IO.Compression proporciona compresión ZIP, GZip y Deflate sin dependencias externas.
🔗 Microsoft — FileSystemWatcher
Sintaxis básica
using var watcher = new FileSystemWatcher("./")
{
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
Filter = "*.txt",
EnableRaisingEvents = true
};
watcher.Created += (s, e) => Console.WriteLine($"Creado: {e.Name}");
watcher.Changed += (s, e) => Console.WriteLine($"Modificado: {e.Name}");
// ZIP
using var archivo = ZipFile.Open("backup.zip", ZipArchiveMode.Create);
archivo.CreateEntryFromFile("datos.txt", "datos.txt");
ZipFile.ExtractToDirectory("backup.zip", "./restaurado");
// GZip
using var original = File.OpenRead("grande.txt");
using var comprimido = File.Create("grande.gz");
using var gzip = new GZipStream(comprimido, CompressionMode.Compress);
await original.CopyToAsync(gzip);
🧪 Cómo probarlo
[Fact]
public void Zip_crea_y_extrae_archivo()
{
var src = Path.GetTempFileName();
var zip = Path.GetTempFileName() + ".zip";
File.WriteAllText(src, "contenido");
using (var a = ZipFile.Open(zip, ZipArchiveMode.Create))
a.CreateEntryFromFile(src, "inner.txt");
Assert.True(new FileInfo(zip).Length > 0);
File.Delete(src); File.Delete(zip);
}
💡 Memoria y rendimiento
FileSystemWatcherusa eventos del SO — no consume CPU hasta que hay cambiosGZipStreamcomprime ≈100 MB/s en CPU moderna;DeflateStreamsimilar sin checksum- ZIP añade overhead de cabeceras (~1 KB por archivo)
✅ Buenas prácticas
- ✅ Limita
NotifyFiltera lo necesario para reducir eventos duplicados - ✅ Comprime logs rotados tras N días para ahorrar espacio
- ⚠️
FileSystemWatcherpuede perder eventos si el buffer interno (64 KB) se llena - ❌ No confíes en el orden de eventos — bufferiza cambios antes de procesar
🏗️ Metodología
- Backups: ZIP con
CreateEntryFromFiley fecha en el nombre - Watch de directorio: procesa eventos con un buffer de tiempo (debounce 500ms)
- Logs: comprime con GZip tras rotación diaria
🔗 Para saber más
Serialización: JSON, CSV y YAML
¿Qué es?
La serialización convierte objetos a formato textual/binario para almacenamiento o transmisión. System.Text.Json es el serializador oficial para JSON (desde .NET Core 3.0). CsvHelper y YamlDotNet son librerías externas para CSV y YAML.
🔗 Microsoft — System.Text.Json
Sintaxis básica
using System.Text.Json;
public record Producto(string Nombre, decimal Precio, int Stock);
// Serializar
var producto = new Producto("Laptop", 999.99m, 10);
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string json = JsonSerializer.Serialize(producto, options);
// Deserializar
var deserializado = JsonSerializer.Deserialize<Producto>(json);
// JsonDocument — acceso sin tipado
using var doc = JsonDocument.Parse(json);
string nombre = doc.RootElement.GetProperty("nombre").GetString()!;
// Source generator — cero reflexión
[JsonSerializable(typeof(Producto[]))]
partial class AppJsonContext : JsonSerializerContext { }
dotnet add package CsvHelper
using CsvHelper;
using System.Globalization;
var records = new List<Producto> { new("Laptop", 999.99m, 10) };
using var writer = new StreamWriter("productos.csv");
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.WriteRecords(records);
dotnet add package YamlDotNet
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
var serializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
string yaml = serializer.Serialize(producto);
🧪 Cómo probarlo
[Fact]
public void Json_deserializa_correctamente()
{
string json = """{"nombre":"Laptop","precio":999.99,"stock":10}""";
var prod = JsonSerializer.Deserialize<Producto>(json);
Assert.Equal("Laptop", prod!.Nombre);
}
[Fact]
public void Source_generator_no_usa_reflexion()
{
var prod = new Producto("Test", 1, 1);
var json = JsonSerializer.Serialize(prod, AppJsonContext.Default.Producto);
Assert.NotNull(json);
}
💡 Memoria y rendimiento
System.Text.Jsones ~50% más rápido que Newtonsoft.Json y con menos allocations- Source generators eliminan reflexión y JIT compilation — ideales para NativeAOT
Utf8JsonReaderyUtf8JsonWritertrabajan directamente sobre UTF-8 sin decodificar
✅ Buenas prácticas
- ✅ Usa
JsonNamingPolicy.CamelCasepara APIs REST - ✅ Para alto rendimiento, usa
JsonSerializerContext(source generators) - ⚠️ No serialices tipos circulares sin configuración de referencia
- ❌ No compartas
JsonSerializerOptionsentre hilos sin inmutabilidad
🏗️ Metodología
- APIs REST: System.Text.Json con camelCase +
[JsonIgnore]en propiedades sensibles - Config: JSON con
WriteIndented = truepara legibilidad humana - Bulk data: CSV con CsvHelper; YAML para configs complejas
🔗 Para saber más
Algoritmos y estructuras de datos
Ordenación y búsqueda
¿Qué es?
La ordenación organiza elementos según un criterio de comparación. Array.Sort usa Introsort (QuickSort + HeapSort + InsertionSort), O(n log n) promedio. BinarySearch requiere datos ordenados y es O(log n). LINQ ofrece OrderBy/ThenBy con ordenación estable.
Sintaxis básica
int[] arr = [5, 3, 8, 1, 9, 2, 7, 4, 6];
// Introsort
Array.Sort(arr);
Array.Sort(arr, (a, b) => b.CompareTo(a)); // Descendente
// LINQ — estable
var ordenado = arr.OrderBy(x => x).ToArray();
var compuesto = personas
.OrderBy(p => p.Nombre)
.ThenByDescending(p => p.Edad);
// Búsqueda binaria — array debe estar ordenado
int index = Array.BinarySearch(arr, 7);
// Búsqueda secuencial con LINQ
var encontrado = arr.FirstOrDefault(n => n > 5);
var rango = arr.Where(n => n > 3 && n < 11).ToList();
// IComparer personalizado
class ComparadorInverso : IComparer<int>
{
public int Compare(int x, int y) => y.CompareTo(x);
}
Array.Sort(arr, new ComparadorInverso());
🧪 Cómo probarlo
[Fact]
public void Sort_ordena_ascendente()
{
int[] actual = [3, 1, 2];
Array.Sort(actual);
Assert.Equal([1, 2, 3], actual);
}
[Theory]
[InlineData(new[] { 1, 3, 5, 7 }, 5, 2)]
[InlineData(new[] { 1, 3, 5, 7 }, 4, -1)] // no encontrado
public void BinarySearch_devuelve_indice(int[] arr, int valor, int esperado)
{
Assert.Equal(esperado, Array.BinarySearch(arr, valor));
}
💡 Memoria y rendimiento
Array.Sortes O(n log n) promedio, O(n²) worst-case raro (mitigado por Introsort)BinarySearches O(log n) — mucho más rápido queFirstOrDefaultO(n)OrderByLINQ es estable (preserva orden relativo) pero genera allocation extraArray.Sortin-place, sin allocations; LINQ crea nuevas colecciones
✅ Buenas prácticas
- ✅ Usa
Array.Sortpara arrays grandes cuando no necesites estabilidad - ✅ Usa
OrderBy/ThenBypara ordenación compuesta y estable - ⚠️ No llames
BinarySearchsin asegurar que el array está ordenado - ❌ No uses
FirstOrDefaultcomo búsqueda en colecciones grandes dondeHashSet/Dictionarysería más eficiente
🏗️ Metodología
- Ordenar en memoria:
Array.Sortpara arrays,List.Sortpara listas - Ordenar para UI: LINQ
OrderBy(estable, predecible) - Búsqueda frecuente: índice con
DictionaryoHashSet - Búsqueda binaria: cuando tienes datos ordenados y estáticos
🔗 Para saber más
- [Microsoft — Sorting in .NET](https://learn.microsoft.com/en-us/dot api/system.array.sort)
Colecciones genéricas y Big O
¿Qué es?
Las colecciones genéricas (List<T>, HashSet<T>, Dictionary<,>, PriorityQueue<,>) ofrecen estructuras de datos con garantías de rendimiento definidas. Elegir la correcta es clave para el rendimiento de cualquier aplicación.
🔗 Microsoft — Collections complexity
Sintaxis básica
// HashSet<T> — O(1) Contains
var hashSet = new HashSet<int> { 1, 2, 3 };
hashSet.Add(4);
bool existe = hashSet.Contains(2);
// Dictionary<K,V> — O(1) por clave
var dict = new Dictionary<string, int> { ["Ana"] = 30 };
dict.TryGetValue("Ana", out int edad);
// SortedDictionary — árbol rojo-negro, O(log n)
var sorted = new SortedDictionary<int, string> { [3] = "tres", [1] = "uno" };
// PriorityQueue (.NET 6+)
var pq = new PriorityQueue<string, int>();
pq.Enqueue("bajo", 3);
pq.Enqueue("alto", 1);
Console.WriteLine(pq.Dequeue()); // "alto"
// LINQ agregación
var suma = nums.Aggregate((a, b) => a + b);
| Operación | List<T> | HashSet<T> | Dictionary<,> | SortedDict<,> | SortedSet<T> | PriorityQueue |
|---|---|---|---|---|---|---|
| Add | O(1)* | O(1) | O(1) | O(log n) | O(log n) | O(log n) |
| Remove | O(n) | O(1) | O(1) | O(log n) | O(log n) | O(log n) |
| Contains | O(n) | O(1) | O(1) | O(log n) | O(log n) | - |
| Access idx | O(1) | - | - | - | - | - |
| Min/Max | O(n) | O(n) | O(n) | O(log n) | O(log n) | O(1) peek |
*O(n) cuando el array interno se redimensiona.
🧪 Cómo probarlo
[Fact]
public void Dictionary_inserta_y_recupera()
{
var d = new Dictionary<string, int> { ["clave"] = 42 };
Assert.True(d.TryGetValue("clave", out int val));
Assert.Equal(42, val);
}
[Fact]
public void PriorityQueue_prioriza_menor_valor()
{
var pq = new PriorityQueue<string, int>();
pq.Enqueue("bajo", 10);
pq.Enqueue("alto", 1);
Assert.Equal("alto", pq.Dequeue());
}
💡 Memoria y rendimiento
List<T>usa array interno que duplica tamaño al llenarse (evita alocar conTrimExcess)DictionaryyHashSetusan tablas hash con buckets — factor de carga ~0.75SortedDictionaryySortedSetson árboles rojo‑negro — más lentos que hash pero ordenadosPriorityQueueusa heap binario — excelente para colas de prioridad
✅ Buenas prácticas
- ✅ Prefiere
Dictionarypara lookup por clave,HashSetpara pertenencia - ✅ Si necesitas orden, usa
SortedSetoSortedDictionary - ⚠️ No uses
Listcuando necesitesContainsfrecuente — pasa aHashSet - ❌ No iteres y modifiques una colección en el mismo
foreach— usaToList()para copia
🏗️ Metodología
- Datos únicos sin orden:
HashSet<T> - Lookup por clave:
Dictionary<K, V> - Cola FIFO:
Queue<T>; pila LIFO:Stack<T> - Prioridad:
PriorityQueue<TElement, TPriority>(.NET 6+) - Orden natural:
SortedSet<T>oSortedDictionary<K, V>
🔗 Para saber más
Ejemplo completo de POO en C#
Este ejemplo une todos los conceptos de POO explicados arriba en un sistema realista de biblioteca multimedia. Léelo de arriba a abajo: cada clase añade una capa.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
// ── Enum y Records (modelos de datos) ──
enum FormatoVideo
{
MP4,
WEBM,
AVI,
MKV
}
record Metadatos
{
public string Titulo { get; init; }
public int DuracionSegundos { get; init; }
public DateTime FechaCreacion { get; init; } = DateTime.Now;
public List<string> Tags { get; init; } = [];
public Metadatos(string titulo, int duracionSegundos)
{
Titulo = titulo;
DuracionSegundos = duracionSegundos;
}
public string DuracionFormateada =>
$"{DuracionSegundos / 60}:{DuracionSegundos % 60:D2}";
public override string ToString() => $"{Titulo} ({DuracionFormateada})";
}
// ── Clase abstracta base ──
abstract class Contenido
{
public Metadatos Metadatos { get; }
public int Reproducciones { get; private set; }
protected Contenido(Metadatos metadatos)
{
Metadatos = metadatos;
}
public string Reproducir()
{
Reproducciones++;
return $"▶️ Reproduciendo {Metadatos.Titulo}...";
}
public abstract string ObtenerInfo();
public override string ToString() => $"[{GetType().Name}] {Metadatos}";
}
// ── Herencia simple ──
sealed class Audio : Contenido
{
public int BitrateKbps { get; }
public Audio(Metadatos metadatos, int bitrateKbps) : base(metadatos)
{
BitrateKbps = bitrateKbps;
}
public override string ObtenerInfo() =>
$"🎵 {Metadatos.Titulo} | {Metadatos.DuracionFormateada} | {BitrateKbps}kbps | {Reproducciones} reproducciones";
}
class Video : Contenido
{
public string Resolucion { get; }
public FormatoVideo Formato { get; }
public bool Subtitulos { get; set; }
public Video(Metadatos metadatos, string resolucion, FormatoVideo formato)
: base(metadatos)
{
Resolucion = resolucion;
Formato = formato;
}
public override string ObtenerInfo() =>
$"🎬 {Metadatos.Titulo} | {Resolucion} | {Formato} | {(Subtitulos ? "CC" : "Sin subtítulos")}";
}
// ── Interfaz (contrato) ──
interface IExportable
{
string Exportar(string formato = "json");
}
// ── Herencia + interfaz ──
class Podcast : Audio, IExportable
{
public int Episodio { get; }
public string Anfitrion { get; }
public Podcast(Metadatos metadatos, int bitrateKbps, int episodio, string anfitrion)
: base(metadatos, bitrateKbps)
{
Episodio = episodio;
Anfitrion = anfitrion;
}
public override string ObtenerInfo() =>
$"🎙️ Ep.{Episodio}: {Metadatos.Titulo} | con {Anfitrion} | {base.ObtenerInfo()}";
public string Exportar(string formato = "json")
{
if (formato == "json")
return JsonSerializer.Serialize(this, new JsonSerializerOptions
{
WriteIndented = true
});
return $"Exportación a {formato} no soportada";
}
}
// ── Composición: un objeto contiene otros objetos ──
class ListaReproduccion
{
public string Nombre { get; }
private readonly List<Contenido> _items = [];
public ListaReproduccion(string nombre) => Nombre = nombre;
public void Agregar(Contenido item) => _items.Add(item);
public bool Quitar(int indice)
{
if (indice < 0 || indice >= _items.Count) return false;
_items.RemoveAt(indice);
return true;
}
public List<string> ReproducirTodo() =>
_items.Select(item => item.Reproducir()).ToList();
public List<string> InfoCompleta() =>
_items.Select(item => item.ObtenerInfo()).ToList();
public int Count => _items.Count;
public Contenido this[int index] => _items[index];
public override string ToString() => $"📋 {Nombre} ({_items.Count} items)";
}
// ── Interfaz para polimorfismo estructural ──
interface IReproducible
{
string Reproducir();
Metadatos Metadatos { get; }
}
// Método que acepta cualquier IReproducible
void Emitir(IReproducible contenido)
{
Console.WriteLine($"📡 Emitiendo: {contenido.Reproducir()}");
}
// ── Uso del sistema ──
// Crear contenido de distintos tipos
var cancion = new Audio(
new Metadatos("Bohemian Rhapsody", 354)
{
Tags = ["rock", "clásico"]
},
bitrateKbps: 320
);
var pelicula = new Video(
new Metadatos("Inception", 8880)
{
Tags = ["ciencia ficción", "thriller"]
},
resolucion: "4K",
formato: FormatoVideo.MKV
)
{
Subtitulos = true
};
var podcast = new Podcast(
new Metadatos("El futuro de la IA", 2700)
{
Tags = ["tecnología", "IA"]
},
bitrateKbps: 128,
episodio: 42,
anfitrion: "Ana López"
);
// Polimorfismo: cada uno implementa ObtenerInfo() a su manera
foreach (var contenido in new Contenido[] { cancion, pelicula, podcast })
Console.WriteLine(contenido.ObtenerInfo());
// Composición
var lista = new ListaReproduccion("Favoritos");
lista.Agregar(cancion);
lista.Agregar(pelicula);
lista.Agregar(podcast);
Console.WriteLine(lista);
Console.WriteLine(string.Join("\n", lista.ReproducirTodo()));
Console.WriteLine(string.Join("\n", lista.InfoCompleta()));
// Interfaz
Emitir(pelicula); // Funciona porque Video implementa IReproducible
// Record + método
Console.WriteLine(podcast.Metadatos.DuracionFormateada); // "45:00"
// Interfaz: exportación
Console.WriteLine(podcast.Exportar("json"));
// Pattern matching
foreach (var item in new Contenido[] { cancion, pelicula, podcast })
{
string tipo = item switch
{
Podcast p => $"Podcast Ep.{p.Episodio}",
Video v => $"Video {v.Resolucion}",
Audio a => $"Audio {a.BitrateKbps}kbps",
_ => "Desconocido"
};
Console.WriteLine(tipo);
}
Conceptos demostrados:
| Concepto | Cómo aparece |
|---|---|
| Clase abstracta | Contenido con abstract string ObtenerInfo() |
| Herencia simple | Audio(Contenido), Video(Contenido) |
| Clase sealed | sealed class Audio — no se puede heredar |
| Interfaz | IExportable, IReproducible |
| Record | Metadatos con init properties |
| Encapsulación | Reproducciones { get; private set; } |
| Polimorfismo | ObtenerInfo() se comporta distinto según la clase concreta |
| Composición | ListaReproduccion contiene List<Contenido> |
| Pattern matching | item switch { Podcast p => ... } |
| Propiedades calculadas | DuracionFormateada (expression-bodied) |
| Colecciones con initializers | Tags = ["rock", "clásico"] (collection expressions C# 12) |
Patrones de diseño con ejemplos
Strategy
Permite intercambiar algoritmos en tiempo de ejecución. En C# puedes usar delegados o interfaces.
// Con delegados (simplificado)
var ordenarAscendente = (List<int> datos) => datos.OrderBy(x => x).ToList();
var ordenarDescendente = (List<int> datos) => datos.OrderByDescending(x => x).ToList();
class Analizador
{
private readonly Func<List<int>, List<int>> _estrategia;
public Analizador(Func<List<int>, List<int>> estrategia)
{
_estrategia = estrategia;
}
public List<int> Analizar(List<int> datos) => _estrategia(datos);
}
var a = new Analizador(ordenarDescendente);
Console.WriteLine(string.Join(", ", a.Analizar([3, 1, 4, 1, 5, 9])));
// Con interfaz (más formal)
interface IEstrategiaOrdenacion
{
List<int> Ordenar(List<int> datos);
}
class OrdenacionAscendente : IEstrategiaOrdenacion
{
public List<int> Ordenar(List<int> datos) =>
[.. datos.OrderBy(x => x)];
}
class OrdenacionDescendente : IEstrategiaOrdenacion
{
public List<int> Ordenar(List<int> datos) =>
[.. datos.OrderByDescending(x => x)];
}
class AnalizadorStrategy
{
private readonly IEstrategiaOrdenacion _estrategia;
public AnalizadorStrategy(IEstrategiaOrdenacion estrategia) => _estrategia = estrategia;
public List<int> Analizar(List<int> datos) => _estrategia.Ordenar(datos);
}
Cuándo usarlo: tienes múltiples variantes de un algoritmo y quieres cambiarlas sin condicionales. En C#, los delegados Func<> y Action<> hacen que a menudo no necesites la interfaz.
Observer
Un objeto (sujeto) notifica a múltiples dependientes (observadores) cuando cambia su estado.
// Con eventos de C#
class Sensor
{
public event EventHandler<double>? OnTemperaturaCambio;
private double _temperatura;
public double Temperatura
{
get => _temperatura;
set
{
_temperatura = value;
OnTemperaturaCambio?.Invoke(this, value);
}
}
}
// Observadores
void Logger(object? sender, double temp) =>
Console.WriteLine($"[LOG] Temperatura: {temp}°C");
void Dashboard(object? sender, double temp) =>
Console.WriteLine($"[DASH] Actualizando gráfico con {temp}°C");
// Uso
var sensor = new Sensor();
sensor.OnTemperaturaCambio += Logger;
sensor.OnTemperaturaCambio += Dashboard;
sensor.Temperatura = 23.5;
Cuándo usarlo: eventos del sistema, notificaciones en tiempo real, desacoplamiento de componentes. Los eventos de C# ya implementan el patrón Observer de serie.
Singleton (con Lazy<T>)
// La forma moderna y thread-safe
public sealed class Configuracion
{
private static readonly Lazy<Configuracion> _instancia =
new(() => new Configuracion());
public static Configuracion Instance => _instancia.Value;
private Configuracion()
{
Console.WriteLine("Cargando configuración (una sola vez)");
}
public string DbConnection { get; set; } = "Server=localhost;Database=myapp";
public bool Debug { get; set; } = false;
}
// Uso
var config = Configuracion.Instance;
config.Debug = true;
Lazy<T> garantiza inicialización thread-safe sin locks explícitos. Es la implementación recomendada en C# moderno.
Factory (con DI)
interface IPlugin
{
string Ejecutar();
}
class PluginPDF : IPlugin
{
public string Ejecutar() => "Procesando PDF";
}
class PluginCSV : IPlugin
{
public string Ejecutar() => "Procesando CSV";
}
class PluginFactory
{
private readonly Dictionary<string, Func<IPlugin>> _registro = new()
{
["pdf"] = () => new PluginPDF(),
["csv"] = () => new PluginCSV()
};
public IPlugin Crear(string tipo) =>
_registro.TryGetValue(tipo.ToLower(), out var factory)
? factory()
: throw new ArgumentException($"Plugin '{tipo}' no encontrado");
}
// Uso
var factory = new PluginFactory();
var plugin = factory.Crear("pdf");
Console.WriteLine(plugin.Ejecutar());
Builder (fluent)
class Pedido
{
public string Cliente { get; set; }
public List<string> Productos { get; set; } = [];
public string Direccion { get; set; }
public bool EnvioGratis { get; set; }
public override string ToString() =>
$"Pedido de {Cliente}: {string.Join(", ", Productos)} a {Direccion}";
}
class PedidoBuilder
{
private readonly Pedido _pedido = new();
public PedidoBuilder Para(string cliente)
{
_pedido.Cliente = cliente;
return this;
}
public PedidoBuilder AgregarProducto(string producto)
{
_pedido.Productos.Add(producto);
return this;
}
public PedidoBuilder ConDireccion(string direccion)
{
_pedido.Direccion = direccion;
return this;
}
public PedidoBuilder EnvioGratuito(bool gratis)
{
_pedido.EnvioGratis = gratis;
return this;
}
public Pedido Build() => _pedido;
}
// Uso
var pedido = new PedidoBuilder()
.Para("Ana López")
.AgregarProducto("Laptop")
.AgregarProducto("Mouse")
.ConDireccion("Calle Mayor 123")
.EnvioGratuito(true)
.Build();
Console.WriteLine(pedido);
Adapter
// Sistema existente (viejo)
class SistemaViejo
{
public List<string[]> Query(string sql)
{
return [["resultado"]];
}
}
// Sistema nuevo (async)
class SistemaNuevo
{
public async Task<List<string[]>> ExecuteAsync(string query)
{
await Task.Delay(10);
return [["resultado"]];
}
}
// Target interface
interface IDBAdapter
{
List<string[]> Query(string sql);
}
// Adapter para SistemaViejo
class AdaptadorViejo : IDBAdapter
{
private readonly SistemaViejo _backend;
public AdaptadorViejo(SistemaViejo backend) => _backend = backend;
public List<string[]> Query(string sql) => _backend.Query(sql);
}
// Adapter para SistemaNuevo
class AdaptadorNuevo : IDBAdapter
{
private readonly SistemaNuevo _backend;
public AdaptadorNuevo(SistemaNuevo backend) => _backend = backend;
public List<string[]> Query(string sql) =>
_backend.ExecuteAsync(sql).GetAwaiter().GetResult();
}
// Uso
IDBAdapter db = new AdaptadorViejo(new SistemaViejo());
Console.WriteLine(db.Query("SELECT 1")[0][0]);
db = new AdaptadorNuevo(new SistemaNuevo());
Console.WriteLine(db.Query("SELECT 1")[0][0]);
Polimorfismo en detalle
C# tiene varias formas de polimorfismo. Todas coexisten en el mismo código.
1. Polimorfismo por herencia (virtual/override)
class Animal
{
public virtual string Sonido() => "...";
}
class Perro : Animal
{
public override string Sonido() => "Guau";
}
class Gato : Animal
{
public override string Sonido() => "Miau";
}
// Dynamic dispatch en runtime
void HacerSonido(Animal animal)
{
Console.WriteLine(animal.Sonido()); // Llama al método correcto según el tipo real
}
HacerSonido(new Perro()); // Guau
HacerSonido(new Gato()); // Miau
2. Polimorfismo por interfaz
interface IForma
{
double Area();
}
class Circulo : IForma
{
public double Radio { get; }
public Circulo(double radio) => Radio = radio;
public double Area() => Math.PI * Radio * Radio;
}
class Rectangulo : IForma
{
public double Ancho { get; }
public double Alto { get; }
public Rectangulo(double ancho, double alto) => (Ancho, Alto) = (ancho, alto);
public double Area() => Ancho * Alto;
}
void MostrarArea(IForma forma)
{
Console.WriteLine($"Área: {forma.Area():F2}");
}
MostrarArea(new Circulo(5)); // Área: 78.54
MostrarArea(new Rectangulo(3, 4)); // Área: 12.00
3. Polimorfismo paramétrico (genéricos)
// Un método que funciona para cualquier tipo T
T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
Console.WriteLine(Max(3, 7)); // 7 (int)
Console.WriteLine(Max("Ana", "Juan")); // "Juan" (string)
4. Ad-hoc polymorphism (sobrecarga)
class Calculadora
{
public int Sumar(int a, int b) => a + b;
public double Sumar(double a, double b) => a + b;
public string Sumar(string a, string b) => a + b;
public int Sumar(params int[] nums) => nums.Sum();
}
5. Dynamic dispatch con virtual/override (en detalle)
class Base
{
public virtual void MetodoVirtual() => Console.WriteLine("Base.Virtual");
public void MetodoNoVirtual() => Console.WriteLine("Base.NoVirtual");
}
class Derivada : Base
{
public override void MetodoVirtual() => Console.WriteLine("Derivada.Virtual");
public new void MetodoNoVirtual() => Console.WriteLine("Derivada.NoVirtual"); // Shadowing
}
// Uso
Base b = new Derivada();
b.MetodoVirtual(); // "Derivada.Virtual" — dynamic dispatch (vtable)
b.MetodoNoVirtual(); // "Base.NoVirtual" — static dispatch (tipo compile-time)
Derivada d = new Derivada();
d.MetodoNoVirtual(); // "Derivada.NoVirtual"
La vtable (virtual method table) es el mecanismo interno: cada clase tiene una tabla con punteros a las implementaciones virtuales. override actualiza la entrada en la vtable. new crea una nueva entrada separada.
Multimedia y gráficos
System.Drawing (solo Windows)
System.Drawing es la API clásica de GDI+ para Windows. No es multiplataforma (solo funciona en Windows).
using System.Drawing;
// Cargar y redimensionar imagen
using var original = Image.FromFile("foto.jpg");
using var thumbnail = new Bitmap(200, 200);
using var graphics = Graphics.FromImage(thumbnail);
graphics.DrawImage(original, 0, 0, 200, 200);
thumbnail.Save("thumb.jpg");
// Dibujar formas
using var bmp = new Bitmap(400, 300);
using var g = Graphics.FromImage(bmp);
g.Clear(Color.White);
using var pen = new Pen(Color.Blue, 3);
g.DrawRectangle(pen, 50, 50, 200, 100);
g.DrawLine(pen, 0, 0, 400, 300);
g.FillEllipse(Brushes.Red, 150, 100, 100, 100);
bmp.Save("dibujo.png", System.Drawing.Imaging.ImageFormat.Png);
⚠️ Advertencia: System.Drawing solo funciona en Windows. Para servidores Linux/macOS usa SkiaSharp o ImageSharp.
SkiaSharp (multiplataforma)
SkiaSharp es el binding .NET de Skia, el motor gráfico de Google Chrome y Android. Funciona en Windows, Linux, macOS, iOS y Android.
dotnet add package SkiaSharp
using SkiaSharp;
// Crear un canvas y dibujar
using var surface = SKSurface.Create(new SKImageInfo(400, 300));
var canvas = surface.Canvas;
canvas.Clear(SKColors.White);
// Paint (estilo de dibujo)
using var paint = new SKPaint
{
Color = SKColors.Blue,
Style = SKPaintStyle.Fill,
StrokeWidth = 3
};
// Formas
canvas.DrawRect(new SKRect(50, 50, 250, 150), paint);
paint.Color = SKColors.Red;
canvas.DrawCircle(200, 150, 50, paint);
// Texto
paint.Color = SKColors.Black;
paint.TextSize = 24;
canvas.DrawText("Hola SkiaSharp", 50, 250, paint);
// Guardar como PNG
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
using var stream = File.OpenWrite("salida.png");
data.SaveTo(stream);
// Cargar imagen
using var original = SKBitmap.Decode("foto.jpg");
using var redimensionado = original.Resize(new SKImageInfo(200, 200), SKFilterQuality.Medium);
ImageSharp (SixLabors)
ImageSharp es una librería puramente .NET, sin dependencias nativas. Multiplataforma, API moderna.
dotnet add package SixLabors.ImageSharp
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
// Cargar y procesar
using var img = await Image.LoadAsync("foto.jpg");
img.Mutate(ctx => ctx
.Resize(200, 200)
.Grayscale()
.Contrast(1.2f));
await img.SaveAsPngAsync("procesada.png");
// Dibujar texto
img.Mutate(ctx => ctx
.DrawText("Watermark", new Font("Arial", 24), Color.White, new PointF(10, 10)));
OpenTK (OpenGL)
OpenTK provee bindings de OpenGL, OpenAL y GLFW para .NET.
dotnet add package OpenTK
using OpenTK.Graphics.OpenGL4;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
public class Ventana : GameWindow
{
public Ventana() : base(new GameWindowSettings(), new NativeWindowSettings
{
Size = (800, 600),
Title = "Hola OpenTK"
}) { }
protected override void OnLoad()
{
GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f);
base.OnLoad();
}
protected override void OnRenderFrame(FrameEventArgs args)
{
GL.Clear(ClearBufferMask.ColorBufferBit);
SwapBuffers();
base.OnRenderFrame(args);
}
}
// Program.cs
using var ventana = new Ventana();
ventana.Run();
FFmpeg.Autogen
Bindings para FFmpeg desde C#. Permite manipular audio/video a nivel de frames y codecs.
dotnet add package FFmpeg.Autogen
Requiere binarios de FFmpeg nativos (ffmpeg, ffprobe).
NAudio (audio)
Librería para reproducción, grabación y procesamiento de audio en Windows.
dotnet add package NAudio
using NAudio.Wave;
// Reproducir archivo WAV
using var reader = new AudioFileReader("sonido.wav");
using var output = new WaveOutEvent();
output.Init(reader);
output.Play();
// Generar tono
var signal = new SignalGenerator
{
Type = SignalGeneratorType.Sin,
Frequency = 440,
Gain = 0.5,
Duration = TimeSpan.FromSeconds(2)
};
OpenCVSharp (visión artificial)
OpenCVSharp es el binding de OpenCV para C#.
dotnet add package OpenCvSharp4
using OpenCvSharp;
// Cargar y mostrar imagen
using var img = Cv2.ImRead("foto.jpg");
Cv2.ImShow("Ventana", img);
Cv2.WaitKey(0);
// Detección de bordes (Canny)
using var gray = img.CvtColor(ColorConversionCodes.BGR2GRAY);
using var edges = gray.Canny(50, 150);
Cv2.ImWrite("bordes.jpg", edges);
Comparativa
| Librería | Plataforma | Ideal para |
|---|---|---|
| System.Drawing | Solo Windows | Legacy, apps Windows Forms/WPF |
| SkiaSharp | Multiplataforma (Win, Linux, macOS, mobile) | Gráficos 2D, UI personalizada, servidores |
| ImageSharp | Multiplataforma (puro .NET) | Procesamiento de imágenes en servidores |
| OpenTK | Multiplataforma | Juegos 3D, gráficos OpenGL |
| FFmpeg.Autogen | Multiplataforma (necesita FFmpeg) | Procesamiento de video, transcodificación |
| NAudio | Solo Windows | Reproducción/grabación de audio |
| OpenCVSharp | Multiplataforma (necesita OpenCV) | Visión artificial, detección facial, OCR |
Fuente: SkiaSharp docs, ImageSharp, OpenTK, NAudio, OpenCVSharp, FFmpeg.Autogen
Bases de datos
Entity Framework Core
EF Core es el ORM oficial de Microsoft. Code-first, migrations, soporte para múltiples bases de datos.
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Pomelo.EntityFrameworkCore.MySql
using Microsoft.EntityFrameworkCore;
// Modelo
public class Usuario
{
public int Id { get; set; }
public required string Nombre { get; set; }
public required string Email { get; set; }
public List<Post> Posts { get; set; } = [];
}
public class Post
{
public int Id { get; set; }
public required string Titulo { get; set; }
public required string Contenido { get; set; }
public int UsuarioId { get; set; }
public Usuario? Usuario { get; set; }
}
// DbContext
public class AppDbContext : DbContext
{
public DbSet<Usuario> Usuarios => Set<Usuario>();
public DbSet<Post> Posts => Set<Post>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlite("Data Source=app.db");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Usuario)
.WithMany(u => u.Posts)
.HasForeignKey(p => p.UsuarioId);
}
}
// Migrations
// dotnet tool install --global dotnet-ef
// dotnet ef migrations add InitialCreate
// dotnet ef database update
// CRUD
using var db = new AppDbContext();
db.Database.EnsureCreated();
var usuario = new Usuario { Nombre = "Ana", Email = "ana@email.com" };
db.Usuarios.Add(usuario);
db.SaveChanges();
var usuarios = db.Usuarios
.Where(u => u.Email.Contains("@"))
.OrderBy(u => u.Nombre)
.ToList();
// Con relaciones (eager loading)
var posts = db.Posts
.Include(p => p.Usuario)
.Where(p => p.Titulo.Contains("C#"))
.ToList();
Dapper (micro-ORM)
Dapper es un micro-ORM desarrollado por Stack Overflow. Extremadamente rápido, queries SQL manuales.
dotnet add package Dapper
using Dapper;
using System.Data.SQLite;
// Dapper no necesita modelos mapeados — usa tipos C# directamente
public record UsuarioDTO(int Id, string Nombre, string Email);
using var conn = new SQLiteConnection("Data Source=app.db");
// Query → lista de objetos
var usuarios = conn.Query<UsuarioDTO>("SELECT Id, Nombre, Email FROM Usuarios");
// Query con parámetros (seguro, sin SQL injection)
var usuario = conn.QueryFirstOrDefault<UsuarioDTO>(
"SELECT * FROM Usuarios WHERE Id = @Id",
new { Id = 1 });
// Execute (INSERT/UPDATE/DELETE)
conn.Execute(
"INSERT INTO Usuarios (Nombre, Email) VALUES (@Nombre, @Email)",
new { Nombre = "Luis", Email = "luis@email.com" });
// Multiples rows en una llamada
conn.Execute(@"
INSERT INTO Usuarios (Nombre, Email) VALUES (@Nombre, @Email)",
new[] {
new { Nombre = "A", Email = "a@e.com" },
new { Nombre = "B", Email = "b@e.com" }
});
// Stored procedures
var result = conn.Query("usp_ObtenerUsuarios",
commandType: CommandType.StoredProcedure);
ADO.NET (driver puro)
using System.Data.SQLite;
using var conn = new SQLiteConnection("Data Source=app.db");
conn.Open();
using var cmd = new SQLiteCommand("SELECT * FROM Usuarios WHERE Id = @id", conn);
cmd.Parameters.AddWithValue("@id", 1);
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine($"{reader["Nombre"]} - {reader["Email"]}");
}
PostgreSQL con Npgsql
dotnet add package Npgsql
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
// Configuración EF Core con PostgreSQL
public class AppDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseNpgsql("Host=localhost;Database=mydb;Username=user;Password=pass");
}
// Dapper + Npgsql
using var conn = new NpgsqlConnection("Host=localhost;Database=mydb;Username=user;Password=pass");
var usuarios = conn.Query("SELECT * FROM Usuarios");
// PostgreSQL específico: arrays, JSON, PostGIS
var resultado = conn.Query<string>(
"SELECT array_agg(nombre) FROM usuarios");
Comparativa de opciones
| Librería | Async | ORM | Para qué |
|---|---|---|---|
| EF Core | Sí | ORM completo | Aplicaciones con modelo de datos complejo, migraciones automáticas |
| Dapper | Sí | No (micro-ORM) | Rendimiento máximo, queries SQL manuales, APIs de alto tráfico |
| ADO.NET | Sí | No (driver puro) | Control total, acceso raw a BD, legacy |
| Npgsql | Sí | No (driver PgSQL) | Conexión directa a PostgreSQL, features específicas de Pg |
| SqlKata | Sí | Query builder | Entre Dapper y EF, construye queries sin SQL bruto |
| RepoTools | Sí | Scaffolding | Genera código de repositorio a partir de esquema BD |
Migraciones con EF Core
# Instalar herramienta global
dotnet tool install --global dotnet-ef
# Crear migración
dotnet ef migrations add AgregarUsuarios
# Aplicar a BD
dotnet ef database update
# Revertir
dotnet ef database update UltimaMigracionBuena
# Generar script SQL
dotnet ef migrations script -o migracion.sql
# En producción:
dotnet ef database update --connection "ConnectionString"
Fuente: EF Core docs, Dapper GitHub, Npgsql docs
WebSockets
ASP.NET Core SignalR
SignalR es la librería oficial de Microsoft para comunicación en tiempo real. Soporta WebSocket, Server-Sent Events, y Long Polling con fallback automático.
// Hub — componente central de SignalR
public class ChatHub : Hub
{
public async Task EnviarMensaje(string usuario, string mensaje)
{
// Broadcast a todos los clientes conectados
await Clients.All.SendAsync("RecibirMensaje", usuario, mensaje);
}
public async Task EnviarAPrincipal(string mensaje)
{
// Enviar solo al grupo "Principal"
await Clients.Group("Principal").SendAsync("MensajeGrupo", mensaje);
}
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "Principal");
await base.OnConnectedAsync();
}
}
// Program.cs — configuración
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<ChatHub>("/chat");
app.Run();
// Cliente JavaScript
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chat")
.build();
connection.on("RecibirMensaje", (usuario, mensaje) => {
console.log(`${usuario}: ${mensaje}`);
});
await connection.start();
await connection.invoke("EnviarMensaje", "Ana", "Hola!");
WebSocket nativo (sin SignalR)
// Servidor WebSocket con ASP.NET Core
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseWebSockets();
var conexiones = new HashSet<WebSocket>();
app.Map("/ws", async (HttpContext context) =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = 400;
return;
}
var ws = await context.WebSockets.AcceptWebSocketAsync();
conexiones.Add(ws);
try
{
var buffer = new byte[1024];
while (ws.State == WebSocketState.Open)
{
var result = await ws.ReceiveAsync(buffer, CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
break;
// Broadcast a todos
foreach (var conn in conexiones.Where(c => c.State == WebSocketState.Open))
{
await conn.SendAsync(buffer[..result.Count],
result.MessageType, true, CancellationToken.None);
}
}
}
finally
{
conexiones.Remove(ws);
if (ws.State != WebSocketState.Closed)
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed", CancellationToken.None);
}
});
app.Run();
Cliente WebSocket nativo
using System.Net.WebSockets;
async Task EscucharWebSocket()
{
using var ws = new ClientWebSocket();
await ws.ConnectAsync(new Uri("wss://ejemplo.com/ws"), CancellationToken.None);
// Enviar mensaje
var enviar = "Hola servidor"u8.ToArray();
await ws.SendAsync(enviar, WebSocketMessageType.Text, true, CancellationToken.None);
// Recibir respuesta
var buffer = new byte[1024];
var result = await ws.ReceiveAsync(buffer, CancellationToken.None);
var mensaje = System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($"Recibido: {mensaje}");
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Adios", CancellationToken.None);
}
¿Cuándo usar qué?
| Tecnología | Para qué |
|---|---|
| SignalR | Comunicación en tiempo real con fallback automático (WSS → SSE → Long Polling), broadcasting, grupos, escalabilidad con Redis backplane |
| WebSocket nativo | Control total sobre el protocolo, bajo nivel, integración con servicios que no son .NET |
| gRPC streaming | Comunicación bidireccional entre microservicios, contratos con protobuf |
| Azure SignalR Service | Escalar SignalR a miles de conexiones sin gestionar servidores propios |
Fuente: SignalR docs, WebSocket docs
Concurrencia y paralelismo
Task Parallel Library (TPL)
TPL es el marco de trabajo base para concurrencia en .NET. Task representa una operación asíncrona.
// Task.Run — ejecuta en ThreadPool
Task tarea = Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Tarea completada");
});
// Task<T> con resultado
Task<int> calculo = Task.Run(() =>
{
int suma = 0;
for (int i = 0; i < 100; i++) suma += i;
return suma;
});
int resultado = await calculo; // async/await
// O: int resultado = calculo.Result; // BLOQUEA (evitar)
// Esperar múltiples tareas
var tareas = new Task[3];
for (int i = 0; i < 3; i++)
{
int id = i;
tareas[i] = Task.Run(() => ProcessItem(id));
}
await Task.WhenAll(tareas);
Parallel.For, Parallel.ForEach
// Parallel.For — iteración paralela (CPU-bound)
Parallel.For(0, 100, i =>
{
Console.WriteLine($"Procesando {i} en hilo {Environment.CurrentManagedThreadId}");
});
// Parallel.ForEach
var items = Enumerable.Range(0, 100);
Parallel.ForEach(items, item =>
{
var resultado = OperacionCostosa(item);
});
// Control de grado de paralelismo
var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };
Parallel.ForEach(items, options, item => Process(item));
PLINQ (Parallel LINQ)
var numeros = Enumerable.Range(1, 10_000_000);
// Automáticamente paralelizado
var pares = numeros
.AsParallel()
.Where(n => n % 2 == 0)
.Select(n => Math.Sqrt(n))
.ToArray();
// Forzar orden (más lento pero predecible)
var ordenado = numeros
.AsParallel()
.AsOrdered()
.Where(n => n > 1_000_000)
.ToArray();
// Parallel query con merge options
var resultado = numeros
.AsParallel()
.WithMergeOptions(ParallelMergeOptions.NotBuffered) // Resultados tan pronto como estén listos
.Where(EsPrimo)
.Take(10);
Channel<T> (System.Threading.Channels)
Channels es la implementación .NET del patrón productor-consumidor, similar a Go channels.
dotnet add package System.Threading.Channels
using System.Threading.Channels;
var channel = Channel.CreateUnbounded<int>();
// Productor
async Task ProductorAsync(ChannelWriter<int> writer)
{
for (int i = 0; i < 10; i++)
{
await writer.WriteAsync(i);
await Task.Delay(100);
}
writer.Complete();
}
// Consumidor
async Task ConsumidorAsync(ChannelReader<int> reader)
{
await foreach (int item in reader.ReadAllAsync())
{
Console.WriteLine($"Consumido: {item}");
}
}
// Ejecutar productor y consumidor en paralelo
var productor = ProductorAsync(channel.Writer);
var consumidor = ConsumidorAsync(channel.Reader);
await Task.WhenAll(productor, consumidor);
lock y sincronización básica
class ContadorSeguro
{
private int _contador;
private readonly object _lock = new();
public void Incrementar()
{
lock (_lock)
{
_contador++;
}
}
public int Obtener()
{
lock (_lock)
{
return _contador;
}
}
}
// Alternativas modernas:
// - Monitor (lock es sugar syntax para Monitor.Enter/Exit)
// - Mutex (entre procesos)
// - SemaphoreSlim (para limitar acceso concurrente)
// - ReaderWriterLockSlim (lectores múltiples, escritor exclusivo)
// - SpinLock (para esperas muy cortas, evita cambio de contexto)
Interlocked (operaciones atómicas)
class Estadisticas
{
private long _llamadas;
private long _errores;
public void RegistrarLlamada()
{
Interlocked.Increment(ref _llamadas);
}
public void RegistrarError()
{
Interlocked.Increment(ref _errores);
}
public (long Llamadas, long Errores) Obtener()
{
return (Interlocked.Read(ref _llamadas), Interlocked.Read(ref _errores));
}
}
// Otros métodos de Interlocked:
Interlocked.Add(ref valor, 5); // Suma atómica
Interlocked.Exchange(ref valor, 10); // Asignación atómica
Interlocked.CompareExchange(ref valor, nuevo, comparacion); // CAS (Compare-And-Swap)
Interlocked.Decrement(ref valor); // Decremento atómico
Tabla de usos de concurrencia
| Escenario | Herramienta | Ejemplo |
|---|---|---|
| I/O-bound (HTTP, BD, archivos) | async/await + Task | Llamadas a API, consultas BD |
| CPU-bound (cálculos) | Parallel.For / PLINQ | Procesamiento de imágenes, álgebra |
| Productor-consumidor | Channel<T> | Procesamiento de streams, colas en memoria |
| Exclusión mutua (recurso compartido) | lock o Monitor | Contadores, cachés simples |
| Operaciones atómicas | Interlocked | Contadores de rendimiento, flags |
| Sincronización entre procesos | Mutex, Semaphore | Archivos compartidos, IPCs |
| Lectura/escritura (muchos lectores) | ReaderWriterLockSlim | Cachés concurrentes |
| Cola thread-safe para productores múltiples | ConcurrentQueue<T> | Logging, eventos |
| Diccionario thread-safe | ConcurrentDictionary<K,V> | Cachés compartidos |
| Tareas con timeout | CancellationTokenSource | Cancelación de operaciones largas |
| Streams asíncronos | IAsyncEnumerable<T> | Paginación, streaming de datos |
Buenas prácticas de concurrencia
// ✅ Hacer: async/await de arriba a abajo
public async Task<string> ObtenerDatosAsync()
{
using var client = new HttpClient();
return await client.GetStringAsync("https://api.example.com");
}
// ✅ Hacer: usar Task.WhenAll para concurrencia independiente
var t1 = Task1Async();
var t2 = Task2Async();
await Task.WhenAll(t1, t2);
// ✅ Hacer: CancellationToken para operaciones largas
await operacionAsync(cts.Token);
// ❌ Evitar: .Result o .Wait() (deadlock)
// var r = tarea.Result; // ❌
// ❌ Evitar: async void (excepto eventos)
// public async void Procesar() { } // ❌
// ❌ Evitar: Task.Run para operaciones que ya son async
// await Task.Run(() => MetodoAsync()); // ❌
// ❌ Evitar: lock en código async (no compila)
// lock (_lock) { await Something(); } // ❌
Fuente: Task Parallel Library docs, Parallel Programming, Channels docs
Testing y calidad
xUnit / NUnit
¿Qué es?
xUnit es el framework de testing más usado en .NET (el propio Microsoft lo usa internamente). NUnit es otra alternativa popular. Ambos soportan [Fact] (tests simples), [Theory] (tests parametrizados) y [InlineData].
Sintaxis básica
dotnet new xunit -o MiProyecto.Tests
dotnet add MiProyecto.Tests reference MiProyecto
public class CalculadoraTests
{
[Fact]
public void Suma_devuelve_resultado_correcto()
{
var calc = new Calculadora();
var resultado = calc.Sumar(2, 3);
Assert.Equal(5, resultado);
}
[Theory]
[InlineData(1, 2, 3)]
[InlineData(-1, 1, 0)]
[InlineData(0, 0, 0)]
public void Suma_parametrizada(int a, int b, int esperado)
{
var calc = new Calculadora();
Assert.Equal(esperado, calc.Sumar(a, b));
}
}
🧪 Cómo probarlo
dotnet test # Ejecuta todos los tests
dotnet test --filter "Suma" # Filtra por nombre
dotnet test --coverage # Reporte de cobertura
💡 Memoria y rendimiento
- Los tests se ejecutan en proceso separado (
dotnet test) - xUnit corre tests en paralelo por defecto (colecciones diferentes)
[Theory]con[InlineData]es más eficiente que múltiples[Fact]
✅ Buenas prácticas
- ✅ Un test por comportamiento, no por método
- ✅ Usa naming:
[Metodo]_[Escenario]_[ResultadoEsperado] - ⚠️ No uses lógica condicional en tests — cada test debe ser determinista
- ❌ No dependas del orden de ejecución — los tests deben ser independientes
🏗️ Metodología
- Unit tests: prueban una sola clase/método, mocks para dependencias
- Integration tests: prueban varias capas juntas (BD real, API real)
- TDD: escribe el test antes del código — red → green → refactor
🔗 Para saber más
Moq y FluentAssertions
¿Qué es?
Moq es la librería de mocking más popular para .NET. FluentAssertions ofrece aserciones más legibles con sintaxis fluida.
Sintaxis básica
dotnet add package Moq
dotnet add package FluentAssertions
public interface IRepositorio { Task<Usuario?> ObtenerAsync(int id); }
[Fact]
public async Task Servicio_usa_repositorio()
{
var mockRepo = new Mock<IRepositorio>();
mockRepo.Setup(r => r.ObtenerAsync(1))
.ReturnsAsync(new Usuario("Ana"));
var servicio = new UsuarioService(mockRepo.Object);
var usuario = await servicio.ObtenerAsync(1);
usuario.Should().NotBeNull();
usuario!.Nombre.Should().Be("Ana");
mockRepo.Verify(r => r.ObtenerAsync(1), Times.Once);
}
✅ Buenas prácticas
- ✅ Usa
Mock.Of<T>()para configuraciones simples - ✅ Prefiere
Verifypara comprobar interacciones - ⚠️ No abuses de mocks — si necesitas mockear demasiado, el diseño necesita revisión
- ❌ No mockees tipos que no sean interfaces o clases virtuales
🔗 Para saber más
Cobertura y herramientas
# Reporte de cobertura con coverlet
dotnet add package coverlet.msbuild
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
# Análisis de mutaciones con Stryker
dotnet tool install -g dotnet-stryker
stryker run
Conceptos avanzados
Source Generators en producción
Los source generators permiten generar código en tiempo de compilación. Ejemplos reales:
- System.Text.Json: genera serializadores sin reflexión
- Regex source generator: compila regex en build time (
[GeneratedRegex]) - MediatR: registra handlers dinámicamente
[JsonSerializable(typeof(Producto[]))]
partial class AppJsonContext : JsonSerializerContext { }
[GeneratedRegex(@"^[\w-\.]+@[\w-]+\.\w{2,4}$")]
public static partial Regex EmailRegex();
NativeAOT
Compila a binario nativo sin runtime .NET. Ideal para CLIs, serverless, contenedores pequeños.
dotnet publish -r linux-x64 -c Release /p:PublishAot=true
# Produce un binario autocontenido (~2-3 MB)
Limitaciones: no soporta reflexión dinámica, ni carga de assemblies en runtime.
Span<T>, Memory<T> y Collections Marshal
Span<T> es un ref struct que proporciona acceso seguro y eficiente a memoria contigua sin allocations.
Span<int> numeros = stackalloc int[100]; // Stack, sin GC
Span<int> slice = arr.AsSpan(1, 3); // Sin copia
ReadOnlySpan<char> texto = "Hola".AsSpan();
// Collections Marshal — acceso directo al array interno de List<T>
var lista = new List<int> { 1, 2, 3 };
Span<int> span = CollectionsMarshal.AsSpan(lista); // Sin copia
System.Threading.Channels
Channels proporcionan productor-consumidor thread-safe sin locks manuales. Ideales para pipelines de datos.
var channel = Channel.CreateUnbounded<int>();
var writer = channel.Writer;
var reader = channel.Reader;
// Productor
await writer.WriteAsync(42);
writer.Complete();
// Consumidor
await foreach (var item in reader.ReadAllAsync())
Console.WriteLine(item);
Govulncheck y análisis de seguridad
Para C#, usa dotnet list package --vulnerable para detectar dependencias con vulnerabilidades conocidas.
Proyecto final integrador
Gestor de contraseñas CLI
¿Qué es?
Un gestor de contraseñas desde terminal que combina archivos, serialización JSON, criptografía, async I/O y testing. Este proyecto integra todos los conceptos de la guía.
🧪 Cómo probarlo
dotnet new console -o PasswordManager
dotnet add package System.Text.Json
dotnet new xunit -o PasswordManager.Tests
dotnet add PasswordManager.Tests reference PasswordManager
Sintaxis básica
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
record Entry(string Service, string Username, string Password);
class Vault
{
private readonly string _path;
private readonly byte[] _key;
public Vault(string path = "vault.enc")
{
_path = path;
_key = LoadOrCreateKey();
}
private byte[] LoadOrCreateKey()
{
var keyFile = "vault.key";
if (File.Exists(keyFile))
return File.ReadAllBytes(keyFile);
var key = RandomNumberGenerator.GetBytes(32);
File.WriteAllBytes(keyFile, key);
return key;
}
public void Add(Entry entry)
{
var store = new Dictionary<string, Entry>();
if (File.Exists(_path))
{
var encrypted = File.ReadAllBytes(_path);
var decrypted = Decrypt(encrypted);
store = JsonSerializer.Deserialize<Dictionary<string, Entry>>(decrypted) ?? [];
}
store[entry.Service] = entry;
var json = JsonSerializer.Serialize(store);
File.WriteAllBytes(_path, Encrypt(json));
}
public Entry? Get(string service) { /* ... */ }
private byte[] Encrypt(string plaintext) { /* AES */ }
private string Decrypt(byte[] ciphertext) { /* AES */ }
}
private byte[] Encrypt(string plaintext)
{
using var aes = Aes.Create();
aes.Key = _key;
aes.GenerateIV();
using var encryptor = aes.CreateEncryptor();
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
var ciphertext = encryptor.TransformFinalBlock(plaintextBytes, 0, plaintextBytes.Length);
var result = new byte[aes.IV.Length + ciphertext.Length];
Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length);
Buffer.BlockCopy(ciphertext, 0, result, aes.IV.Length, ciphertext.Length);
return result;
}
private string Decrypt(byte[] ciphertext)
{
using var aes = Aes.Create();
aes.Key = _key;
var iv = new byte[aes.IV.Length];
Buffer.BlockCopy(ciphertext, 0, iv, 0, iv.Length);
aes.IV = iv;
using var decryptor = aes.CreateDecryptor();
var decrypted = decryptor.TransformFinalBlock(ciphertext, iv.Length, ciphertext.Length - iv.Length);
return Encoding.UTF8.GetString(decrypted);
}
}
Web scraper con async (nivel 60)
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
record Article(string Title, string Url, string Source);
class WebScraper
{
private readonly HttpClient _http;
public WebScraper()
{
_http = new HttpClient();
_http.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0");
}
public async Task<List<Article>> ScrapeNewsAsync(string url)
{
var response = await _http.GetStringAsync(url);
var articles = new List<Article>();
// Extraer títulos de enlaces (HTML simplificado)
var matches = Regex.Matches(response,
@"<a[^>]*href=""([^""]+)""[^>]*>([^<]+)</a>",
RegexOptions.IgnoreCase);
foreach (Match match in matches)
{
var href = match.Groups[1].Value;
var title = match.Groups[2].Value.Trim();
if (!string.IsNullOrEmpty(title) && title.Length > 10)
{
var fullUrl = href.StartsWith("http") ? href : new Uri(new Uri(url), href).ToString();
articles.Add(new Article(title, fullUrl, url));
}
}
return articles;
}
public async Task<List<Article>> ScrapeMultipleAsync(string[] sources)
{
var tasks = new List<Task<List<Article>>>();
foreach (var source in sources)
tasks.Add(ScrapeNewsAsync(source));
var results = await Task.WhenAll(tasks);
var all = new List<Article>();
foreach (var r in results)
all.AddRange(r);
return all;
}
}
// Uso
var scraper = new WebScraper();
var articles = await scraper.ScrapeMultipleAsync([
"https://news.ycombinator.com",
"https://www.python.org/blogs/"
]);
foreach (var a in articles[..10])
Console.WriteLine($"[{a.Source}] {a.Title}");
Despliegue a producción
Build para producción
dotnet publish -c Release -o ./publish
Docker multi-stage
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
WORKDIR /app
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o ./publish
FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=builder /app/publish .
CMD ["dotnet", "MiApp.dll"]
Hosting
- Azure — Azure App Service con despliegue directo desde GitHub
- Fly.io — despliegue con
flyctl launch - Railway — conecta tu repo y despliega automáticamente
CI/CD (GitHub Actions)
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0'
- run: dotnet publish -c Release -o ./publish
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
Hacks y tips de productividad
1. Primary constructors (C# 12)
// Antes:
public class Persona
{
public string Nombre { get; }
public int Edad { get; }
public Persona(string nombre, int edad) { Nombre = nombre; Edad = edad; }
}
// Ahora:
public class Persona(string nombre, int edad)
{
public string Nombre => nombre;
public int Edad => edad;
public string Saludar() => $"Hola, soy {nombre}";
}
// También funciona con structs y records
public struct Punto(double x, double y)
{
public double DistanciaAlOrigen => Math.Sqrt(x * x + y * y);
}
2. Raw string literals (C# 11)
// Sin escapes, ideal para JSON, SQL, XML
var json = """
{
"nombre": "Ana",
"edad": 30,
"direccion": {
"calle": "Calle Mayor",
"numero": 123
}
}
""";
// Con interpolación ($$ para distinguir de { })
var nombre = "Ana";
var saludo = $$"""
{
"mensaje": "Hola {{nombre}}",
"fecha": "{{DateTime.Now:yyyy-MM-dd}}"
}
""";
3. List patterns y slice patterns (C# 11)
int[] numeros = [1, 2, 3, 4, 5];
// Comprobar inicio/final
if (numeros is [1, 2, ..]) // Empieza con 1, 2
Console.WriteLine("Empieza con 1, 2");
if (numeros is [.., 4, 5]) // Termina con 4, 5
Console.WriteLine("Termina con 4, 5");
// Extraer elementos
if (numeros is [var primero, .., var ultimo])
Console.WriteLine($"Primero: {primero}, último: {ultimo}");
// Match en switches
string Describir(int[] nums) => nums switch
{
[] => "Vacío",
[var n] => $"Un elemento: {n}",
[var a, var b] => $"Dos elementos: {a} y {b}",
[..] and [_, _, ..] => "Tres o más elementos"
};
4. Collection expressions (C# 12)
// Antes:
var lista = new List<int> { 1, 2, 3 };
int[] arr = new int[] { 1, 2, 3 };
// Ahora — sintaxis unificada con []
List<int> lista = [1, 2, 3];
int[] arr = [1, 2, 3];
Span<int> span = [1, 2, 3];
IEnumerable<int> enumerable = [1, 2, 3];
// Spread operator (..)
List<int> otros = [.. lista, 4, 5, .. arr];
// [1, 2, 3, 4, 5, 1, 2, 3]
// Inicializadores de diccionario
Dictionary<string, int> dict = [
["Ana"] = 30,
["Juan"] = 25
];
5. File-scoped namespaces (C# 10)
// Todo el archivo está en este namespace — menos indentación
namespace MiApp.Modelos;
public class Usuario { }
public class Producto { }
6. Required members (C# 11)
public class Producto
{
public required string Nombre { get; init; }
public required decimal Precio { get; init; }
public string? Descripcion { get; init; }
}
// El compilador OBLIGA a inicializar Nombre y Precio
var p = new Producto
{
Nombre = "Laptop", // Obligatorio
Precio = 1000m // Obligatorio
};
7. Alias global para tipos
// GlobalUsings.cs
global using Sys = System;
global using Json = System.Text.Json;
global using Cts = System.Threading.CancellationToken;
// En cualquier archivo:
Sys.Console.WriteLine("Hola");
var json = Json.JsonSerializer.Serialize(obj);
8. [GeneratedRegex] — Regex en compile-time (C# 11)
public partial class EmailHelper
{
[GeneratedRegex(@"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")]
public static partial Regex EmailRegex();
public static bool EsValido(string email) => EmailRegex().IsMatch(email);
}
// Sin reflexión, sin compilación runtime — el patrón se compila en build
9. BenchmarkDotNet — medir rendimiento
dotnet add package BenchmarkDotNet
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser]
public class Benchmarks
{
private readonly int[] _data = Enumerable.Range(0, 1000).ToArray();
[Benchmark(Baseline = true)]
public int ForLoop()
{
int sum = 0;
for (int i = 0; i < _data.Length; i++)
sum += _data[i];
return sum;
}
[Benchmark]
public int ForEachLoop()
{
int sum = 0;
foreach (var n in _data)
sum += n;
return sum;
}
[Benchmark]
public int LinqSum() => _data.Sum();
}
// Program.cs
BenchmarkRunner.Run<Benchmarks>();
10. dotnet format y análisis estático
# Formatear código
dotnet format
# Análisis de estilo y calidad
dotnet format --verify-no-changes # CI: error si no está formateado
dotnet analyze # Análisis Roslyn
# En csproj:
# <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
# <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
Fuente: C# 12 features, BenchmarkDotNet docs, C# 11 features
Canales y recursos en español
YouTube
- Hector de Leon (@hector-de-leon) — Tutoriales de .NET, ASP.NET Core, Azure, C# moderno. Explica conceptos complejos de forma clara y con ejemplos prácticos
- .NET Dev (@dotnetdev) — Contenido variado sobre desarrollo .NET, desde fundamentos hasta arquitectura
- CodigoDotNet (@codigodotnet) — C#, .NET Core, Entity Framework, patrones de diseño, buenas prácticas
- dotNet Argentina (@dotnetargentina) — Canal argentino con streams, tutoriales, y contenido de la comunidad .NET en español
- MoureDev — Aunque es multi-lenguaje, tiene contenido sobre C# y .NET
- Midulive — Desarrollo en general, incluye contenido de C# y .NET
Comunidades
- r/csharp (Reddit) — Comunidad global de C#, noticias, preguntas, proyectos. En inglés pero muy activa y útil
- r/dotnet (Reddit) — Noticias y discusiones sobre el ecosistema .NET
- Discord .NET en español — Canales de ayuda, proyectos, mentoría en tiempo real
- Stack Overflow en español — Etiqueta
c#para dudas técnicas - .NET España — Asociación, eventos, conferencias (dotnetspain.com)
Repositorios y blogs
- dotnet/csharplang — Repositorio oficial del lenguaje C#, propuestas (RFCs), discusiones de diseño
- dotnet/aspnetcore — Código fuente de ASP.NET Core
- dotnet/efcore — Entity Framework Core
- Nick Chapsas YouTube — Mejor canal inglés sobre performance y C# moderno
- C# in Depth (Jon Skeet) — Libro de referencia sobre la evolución de C#
- Awesome .NET — Lista curada de frameworks, librerías y recursos .NET
Fuente: búsqueda en YouTube y Reddit, listas curadas de Awesome .NET
⚠️ Errores comunes
Si algo falla, lo más probable es que esté en esta tabla:
| Error | Causa | Solución |
|---|---|---|
CS0103: The name 'X' does not exist in the current context | Variable o miembro no declarado, o falta un using / namespace | Declara la variable o añade el using correspondiente; revisa la ortografía |
CS0161: not all code paths return a value | Un método con retorno no devuelve valor en todas las ramas | Añade return en todas las ramas del if/else o cambia el retorno a void |
CS1061: 'X' does not contain a definition for 'Y' | Llamas a un método o propiedad que no existe en el tipo | Revisa el nombre y los argumentos; comprueba la versión del paquete NuGet |
NullReferenceException: Object reference not set to an instance of an object | Accedes a una referencia null (propiedad, método, índice) | Usa ?. (null-conditional), ?? (null-coalescing) o valida antes de usar |
FileNotFoundException / Could not load file or assembly | Falta un ensamblado o la ruta es incorrecta en runtime | Verifica que el paquete NuGet está instalado y que la versión coincide |
NU1101: Unable to find package 'X' | NuGet no encuentra el paquete en los feeds configurados | Comprueba la URL del feed en nuget.config; ejecuta dotnet nuget list source |
dotnet: command not found | El SDK .NET no está instalado o no está en el PATH | Instala el SDK desde dotnet.microsoft.com/download y abre una terminal nueva |
🔗 Guías relacionadas
- Java de 0 a 100 — si también dominas la JVM, complementa con Guía de Java: De 0 a 100
- TypeScript de 0 a 100 — tipado estático en el mundo web: Guía de TypeScript: De 0 a 100
- Docker — despliega tus apps .NET en contenedores: Docker: instalación y primeros pasos
Referencias y documentación oficial
- Docs oficiales: learn.microsoft.com/en-us/dotnet/csharp — tutorial, language reference, API reference
- .NET API Browser: learn.microsoft.com/en-us/dotnet/api — busca cualquier clase/método de .NET
- Roslyn (compilador): github.com/dotnet/roslyn — código fuente del compilador de C#
- .NET Runtime: github.com/dotnet/runtime — CLR, BCL, GC, JIT
- ASP.NET Core: learn.microsoft.com/en-us/aspnet/core — documentación oficial del framework web
- Entity Framework Core: learn.microsoft.com/en-us/ef — ORM oficial
- NuGet: nuget.org — gestor de paquetes oficial
- C# Language Design: github.com/dotnet/csharplang — propuestas (RFCs), meetings, decisiones de diseño
- .NET Blog: devblogs.microsoft.com/dotnet — novedades, releases, artículos técnicos
- Andreas Nesheim (.NET Blog): Artículos sobre concurrencia, async, y patrones avanzados en C#
Fuentes de esta guía
Esta guía se ha elaborado a partir de las siguientes fuentes consultadas durante su desarrollo:
- Microsoft .NET Docs — Documentación oficial
- ASP.NET Core Docs
- Entity Framework Core Docs
- C# Language Reference
- .NET API Reference
- C# in Depth (Jon Skeet) — referencia sobre la evolución del lenguaje
- Pro .NET Memory Management (Konrad Kokosa) — gestión de memoria y GC
- Architecture Patterns with .NET — guías de arquitectura
- Clean Architecture (Steve Smith) — Microsoft’s guide
- Awesome .NET (GitHub) — lista curada de recursos
- r/csharp — comunidad, debates y proyectos destacados
- r/dotnet — comunidad del ecosistema .NET
- Nick Chapsas YouTube — C# moderno y performance
- Andreas Nesheim Blog — concurrencia y patrones avanzados
- .NET Blog (Microsoft) — novedades oficiales
- dotnet/csharplang (GitHub) — diseño y evolución del lenguaje
🏆 Retos Relacionados
Pon a prueba lo aprendido con estos desafíos:
