Pular para conteúdo

C# 10: referência

Sintaxe e padrões modernos do C# 10 (.NET 6+).

Tipos básicos

// Inteiros
byte b = 255;              // 0 a 255
short s = 32767;           // -32k a 32k
int i = 2147483647;        // -2bi a 2bi
long l = 9223372036854775807L;

// Decimais
float f = 3.14f;           // 7 dígitos
double d = 3.14159265359;  // 15-16 dígitos
decimal m = 19.99m;        // 28-29 dígitos (dinheiro)

// Outros
bool flag = true;
char c = 'A';
string texto = "Hello";

// Nullable
int? nullable = null;
string? nullableStr = null;

Strings

// Interpolação
string nome = "João";
string msg = $"Olá, {nome}!";

// Verbatim (escapa \ automaticamente)
string path = @"C:\Users\João";

// Raw string literals (C# 11+)
string json = """
    {
        "nome": "João",
        "idade": 30
    }
    """;

// Métodos úteis
str.ToUpper();
str.ToLower();
str.Trim();
str.Split(',');
str.Contains("texto");
str.StartsWith("pre");
str.Replace("old", "new");
string.IsNullOrEmpty(str);
string.IsNullOrWhiteSpace(str);
string.Join(", ", lista);

File-scoped namespace

// Antes (C# 9)
namespace MeuProjeto.Services
{
    public class UserService
    {
        // ...
    }
}

// C# 10+
namespace MeuProjeto.Services;

public class UserService
{
    // ...
}

Global usings

// GlobalUsings.cs (ou qualquer arquivo)
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
global using MeuProjeto.Models;

// Agora disponível em todos os arquivos do projeto

Implicit usings

No .NET 6+, habilite <ImplicitUsings>enable</ImplicitUsings> no .csproj para usings automáticos.

Records

// Record class (referência, imutável por padrão)
public record Person(string Name, int Age);

// Uso
var pessoa = new Person("João", 30);
var outra = pessoa with { Age = 31 };  // cria cópia modificada

// Igualdade por valor
var p1 = new Person("João", 30);
var p2 = new Person("João", 30);
Console.WriteLine(p1 == p2);  // true

// Record struct (C# 10)
public readonly record struct Point(int X, int Y);

// Record com corpo
public record Person(string Name, int Age)
{
    public string Greeting => $"Olá, {Name}!";
}

Classes e structs

public class User
{
    // Propriedades
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public string? Email { get; set; }

    // Init-only (imutável após construção)
    public DateTime CreatedAt { get; init; }

    // Computed property
    public bool HasEmail => !string.IsNullOrEmpty(Email);

    // Construtor
    public User(int id, string name)
    {
        Id = id;
        Name = name;
        CreatedAt = DateTime.UtcNow;
    }

    // Métodos
    public void UpdateEmail(string email) => Email = email;
}

// Struct
public struct Coordinate
{
    public double Lat { get; init; }
    public double Lon { get; init; }
}

// Readonly struct (totalmente imutável)
public readonly struct Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }
}

Pattern matching

// is pattern
if (obj is string s)
{
    Console.WriteLine(s.ToUpper());
}

// switch expression
string GetStatus(int code) => code switch
{
    200 => "OK",
    201 => "Created",
    400 => "Bad Request",
    404 => "Not Found",
    >= 500 and < 600 => "Server Error",
    _ => "Unknown"
};

// Property pattern
string GetDiscount(User user) => user switch
{
    { Age: < 18 } => "10%",
    { Age: >= 65 } => "15%",
    { IsPremium: true } => "20%",
    _ => "0%"
};

// Tuple pattern
string GetQuadrant(int x, int y) => (x, y) switch
{
    ( > 0, > 0) => "Q1",
    ( < 0, > 0) => "Q2",
    ( < 0, < 0) => "Q3",
    ( > 0, < 0) => "Q4",
    _ => "Origin or Axis"
};

// List patterns (C# 11+)
int[] numbers = { 1, 2, 3 };
if (numbers is [1, 2, 3])
{
    Console.WriteLine("Match!");
}

if (numbers is [var first, .., var last])
{
    Console.WriteLine($"First: {first}, Last: {last}");
}

Null handling

// Null-conditional
string? nome = pessoa?.Nome;
int? tamanho = lista?.Count;

// Null-coalescing
string valor = texto ?? "default";
lista ??= new List<string>();  // atribui se null

// Null-forgiving (quando você sabe que não é null)
string nome = pessoa!.Nome;

// Required (C# 11+)
public class User
{
    public required string Name { get; set; }
}

Collections

// List
var lista = new List<int> { 1, 2, 3 };
lista.Add(4);
lista.Remove(2);
lista.Contains(3);

// Dictionary
var dict = new Dictionary<string, int>
{
    ["um"] = 1,
    ["dois"] = 2
};
dict.TryGetValue("um", out var valor);

// HashSet
var set = new HashSet<string> { "a", "b" };
set.Add("c");
set.Contains("a");

// Array
int[] arr = { 1, 2, 3 };
int[] arr2 = new int[10];
var span = arr.AsSpan();

// Range e Index
int[] nums = { 0, 1, 2, 3, 4, 5 };
var last = nums[^1];       // 5 (último)
var slice = nums[1..4];    // { 1, 2, 3 }
var fromEnd = nums[^3..];  // { 3, 4, 5 }

LINQ

var users = new List<User>();

// Query syntax
var adults = from u in users
             where u.Age >= 18
             orderby u.Name
             select u;

// Method syntax (preferido)
var adults = users
    .Where(u => u.Age >= 18)
    .OrderBy(u => u.Name)
    .ToList();

// Métodos comuns
users.Where(u => u.Active);
users.Select(u => u.Name);
users.OrderBy(u => u.Name);
users.OrderByDescending(u => u.Age);
users.First();
users.FirstOrDefault();
users.Single();
users.SingleOrDefault();
users.Any(u => u.Admin);
users.All(u => u.Active);
users.Count();
users.Sum(u => u.Score);
users.Average(u => u.Age);
users.Max(u => u.Age);
users.Min(u => u.Age);
users.GroupBy(u => u.Department);
users.Distinct();
users.Take(10);
users.Skip(5);
users.ToList();
users.ToArray();
users.ToDictionary(u => u.Id);

Async/Await

// Método async
public async Task<User> GetUserAsync(int id)
{
    var response = await httpClient.GetAsync($"/users/{id}");
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync<User>();
}

// Async void (apenas para event handlers)
private async void Button_Click(object sender, EventArgs e)
{
    await DoSomethingAsync();
}

// Task.WhenAll (paralelo)
var tasks = ids.Select(id => GetUserAsync(id));
var users = await Task.WhenAll(tasks);

// Task.WhenAny (primeiro a completar)
var first = await Task.WhenAny(task1, task2);

// ConfigureAwait (libraries)
await SomeAsync().ConfigureAwait(false);

// Cancelamento
public async Task DoWork(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        await Task.Delay(1000, ct);
    }
}

// ValueTask (performance)
public ValueTask<int> GetCachedValueAsync()
{
    if (_cache.TryGetValue(key, out var value))
        return ValueTask.FromResult(value);

    return new ValueTask<int>(FetchValueAsync());
}

Lambdas e delegates

// Lambda
Func<int, int> dobro = x => x * 2;
Func<int, int, int> soma = (a, b) => a + b;
Action<string> print = msg => Console.WriteLine(msg);

// Lambda com tipo explícito (C# 10)
var parse = (string s) => int.Parse(s);

// Lambda com atributos (C# 10)
var validate = [Required] (string s) => s.Length > 0;

// Delegate
public delegate void Callback(string message);
Callback cb = msg => Console.WriteLine(msg);

// Events
public event EventHandler<UserEventArgs>? UserCreated;
UserCreated?.Invoke(this, new UserEventArgs(user));

Generics

// Classe genérica
public class Repository<T> where T : class
{
    private readonly List<T> _items = new();

    public void Add(T item) => _items.Add(item);
    public T? GetById(int id) => _items.FirstOrDefault();
}

// Método genérico
public T Parse<T>(string json) where T : new()
{
    return JsonSerializer.Deserialize<T>(json) ?? new T();
}

// Constraints
where T : class          // tipo referência
where T : struct         // tipo valor
where T : new()          // construtor sem parâmetros
where T : BaseClass      // herda de
where T : IInterface     // implementa
where T : notnull        // não-nulo

Interfaces

public interface IRepository<T>
{
    Task<T?> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(int id);
}

// Default interface methods (C# 8+)
public interface ILogger
{
    void Log(string message);

    void LogError(string message) => Log($"ERROR: {message}");
    void LogWarning(string message) => Log($"WARN: {message}");
}

// Implementação
public class UserRepository : IRepository<User>
{
    public async Task<User?> GetByIdAsync(int id)
    {
        // implementação
    }
    // ...
}

Exception handling

try
{
    var result = await ProcessAsync();
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // 404 específico
}
catch (HttpRequestException ex)
{
    logger.LogError(ex, "HTTP error");
    throw;  // re-throw mantendo stack trace
}
catch (Exception ex)
{
    logger.LogError(ex, "Unexpected error");
    throw new ApplicationException("Failed to process", ex);
}
finally
{
    // sempre executa
}

// Throw expression
var user = await GetUserAsync(id) ?? throw new NotFoundException();

Dependency Injection

// Program.cs (.NET 6+)
var builder = WebApplication.CreateBuilder(args);

// Registrar serviços
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddTransient<IEmailService, EmailService>();

// Com factory
builder.Services.AddScoped<IDbConnection>(sp =>
    new SqlConnection(builder.Configuration.GetConnectionString("Default")));

var app = builder.Build();

// Injeção via construtor
public class UserController
{
    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
        _userService = userService;
    }
}

// Primary constructor (C# 12+)
public class UserController(IUserService userService)
{
    public async Task<User> Get(int id) => await userService.GetAsync(id);
}

Minimal API (.NET 6+)

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/users/{id}", async (int id, IUserService service) =>
{
    var user = await service.GetByIdAsync(id);
    return user is not null ? Results.Ok(user) : Results.NotFound();
});

app.MapPost("/users", async (User user, IUserService service) =>
{
    await service.CreateAsync(user);
    return Results.Created($"/users/{user.Id}", user);
});

app.MapPut("/users/{id}", async (int id, User user, IUserService service) =>
{
    await service.UpdateAsync(id, user);
    return Results.NoContent();
});

app.MapDelete("/users/{id}", async (int id, IUserService service) =>
{
    await service.DeleteAsync(id);
    return Results.NoContent();
});

app.Run();

Entity Framework Core

// DbContext
public class AppDbContext : DbContext
{
    public DbSet<User> Users => Set<User>();
    public DbSet<Order> Orders => Set<Order>();

    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>(e =>
        {
            e.HasKey(u => u.Id);
            e.Property(u => u.Name).IsRequired().HasMaxLength(100);
            e.HasMany(u => u.Orders).WithOne(o => o.User);
        });
    }
}

// Queries
var users = await context.Users
    .Where(u => u.Active)
    .Include(u => u.Orders)
    .OrderBy(u => u.Name)
    .ToListAsync();

var user = await context.Users.FindAsync(id);

// Insert
context.Users.Add(new User { Name = "João" });
await context.SaveChangesAsync();

// Update
user.Name = "João Silva";
await context.SaveChangesAsync();

// Delete
context.Users.Remove(user);
await context.SaveChangesAsync();

// Raw SQL
var users = await context.Users
    .FromSqlRaw("SELECT * FROM Users WHERE Active = 1")
    .ToListAsync();

Configuração

// appsettings.json
{
    "ConnectionStrings": {
        "Default": "Server=localhost;Database=App;..."
    },
    "AppSettings": {
        "ApiKey": "xxx",
        "MaxRetries": 3
    }
}

// Classe de configuração
public class AppSettings
{
    public string ApiKey { get; set; } = "";
    public int MaxRetries { get; set; }
}

// Program.cs
builder.Services.Configure<AppSettings>(
    builder.Configuration.GetSection("AppSettings"));

// Uso
public class MyService
{
    private readonly AppSettings _settings;

    public MyService(IOptions<AppSettings> options)
    {
        _settings = options.Value;
    }
}

// Direto
var connectionString = builder.Configuration.GetConnectionString("Default");
var apiKey = builder.Configuration["AppSettings:ApiKey"];

Testes (xUnit)

public class UserServiceTests
{
    private readonly Mock<IUserRepository> _repoMock;
    private readonly UserService _service;

    public UserServiceTests()
    {
        _repoMock = new Mock<IUserRepository>();
        _service = new UserService(_repoMock.Object);
    }

    [Fact]
    public async Task GetById_ReturnsUser_WhenExists()
    {
        // Arrange
        var user = new User { Id = 1, Name = "João" };
        _repoMock.Setup(r => r.GetByIdAsync(1))
            .ReturnsAsync(user);

        // Act
        var result = await _service.GetByIdAsync(1);

        // Assert
        Assert.NotNull(result);
        Assert.Equal("João", result.Name);
    }

    [Theory]
    [InlineData(1, true)]
    [InlineData(0, false)]
    [InlineData(-1, false)]
    public void IsValidId_ReturnsExpected(int id, bool expected)
    {
        var result = _service.IsValidId(id);
        Assert.Equal(expected, result);
    }
}