Trabalhando com tuplas em C#

No desenvolvimento de software, frequentemente nos deparamos com a necessidade de agrupar diferentes tipos de dados em uma única unidade, sem a necessidade de criar uma estrutura ou classe complexa. Nesse cenário, as tuplas se mostram uma solução bastante eficaz, permitindo armazenar múltiplos valores em uma única estrutura sem a sobrecarga de criar tipos personalizados como classes ou structs. O que são tuplas? Tuplas são estruturas de dados que permitem armazenar múltiplos valores de diferentes tipos em uma única entidade. Elas são leves, simples e eficientes, ideais para agrupar dados temporários ou relacionados de maneira direta. Ao contrário das classes ou structs, que exigem uma definição formal para cada tipo de dado, as tuplas oferecem uma maneira rápida e flexível de organizar informações sem a necessidade de complexidade adicional. Tuplas com Tuple – antes do C# 7 Antes da introdução das tuplas modernas no C# 7, a única forma de utilizar tuplas em C# era por meio da classe “Tuple<T>”. Ela foi introduzida no .NET Framework 4.0 e permitia armazenar até oito valores em uma única tupla, utilizando o tipo genérico “Tuple<T1, T2, …, T8>”, da seguinte forma: var pessoaTupla = new Tuple(1, “João”, true); Console.WriteLine(pessoaTupla.Item1); Console.WriteLine(pessoaTupla.Item2); Console.WriteLine(pessoaTupla.Item3); Limitações do “Tuple<T>”: Nomes de itens: os valores armazenados em uma “Tuple<T>” são acessados por propriedades como “Item1”, “Item2”, e assim por diante. Embora funcionais, esses nomes não são descritivos, o que pode levar a confusões sobre qual valor está sendo acessado, especialmente em tuplas com múltiplos itens.  Imutabilidade: tuplas baseadas em “Tuple<T>” são imutáveis, ou seja, você não pode alterar os valores depois de criá-los. Tuplas no C# 7+ – Uso da sintaxe moderna Com a chegada do C# 7, a linguagem introduziu uma nova forma de trabalhar com tuplas, que são mais simples do que as antigas “Tuple<T>”. Agora, podemos usar a sintaxe de tuplas diretamente com a palavra-chave “var”, o que torna o código mais legível e intuitivo: var pessoaTupla = (1, “João”, true); Console.WriteLine(pessoaTupla.Item1); Console.WriteLine(pessoaTupla.Item2); Console.WriteLine(pessoaTupla.Item3); Nomeação de elementos de tuplas Para sanar o questão da nomenclatura dos itens, no C# 7 foi introduzida a capacidade de nomear os elementos das tuplas diretamente na declaração. Isso torna o código mais expressivo e fácil de entender. var pessoaTupla= (Id: 1, Nome: “João”, Ativo: true); Console.WriteLine(pessoaTupla.Id); Console.WriteLine(pessoaTupla.Nome); Console.WriteLine(pessoaTupla.Ativo); Com isso, é possível chamar a informação desejada pelo nome que foi definido. Inferência de tipos O C# 7 também introduziu a inferência de tipos, o que significa que você pode omitir o tipo da tupla ao usá-la, e o compilador infere os tipos com base nos valores atribuídos. var pessoaTupla= (1, “João”, true); Neste caso, o compilador infere que a tupla contém um “int”, uma “string” e um “bool”, simplificando a declaração sem a necessidade de especificar os tipos manualmente. Retorno de múltiplos valores em métodos Uma das utilizações mais comuns das tuplas é o retorno de múltiplos valores de um método. Antes da introdução das tuplas, se precisássemos retornar mais de um valor de um método, a solução seria criar uma classe ou struct para agrupar os valores. Com as tuplas, esse processo se torna simples e direto. Exemplo: public static (int, string) ObterDados() { return (1, “Valor retornado”); } var resultado = ObterDados(); Console.WriteLine(resultado.Item1); Console.WriteLine(resultado.Item2); Nomeação dos valores no retorno Ao retornar tuplas de métodos, também podemos nomear os elementos, tornando o código ainda mais legível: public static (int Id, string Nome) ObterDados() { return (1, “João”); } var resultado = ObterDados(); Console.WriteLine(resultado.Id); Console.WriteLine(resultado.Nome); Decomposição de tuplas A decomposição de tuplas permite extrair seus valores diretamente para variáveis, tornando o código mais limpo e fácil de entender. var minhaTupla = (1, “João”, true); var (id, nome, ativo) = minhaTupla; Console.WriteLine(id); Console.WriteLine(nome); Console.WriteLine(ativo); Essa forma de decomposição é bastante útil quando trabalhamos com tuplas retornadas de métodos. Boas práticas Embora as tuplas sejam bastante práticas, é importante usá-las com cautela para evitar confusão e manter a clareza no código. Aqui estão algumas boas práticas: Evite o uso excessivo de tuplas: quando o número de valores a ser retornado ou agrupado é grande ou a semântica é complexa, considere usar structs ou classes. Nomeie os elementos da tupla: sempre que possível, use a nomeação de elementos em tuplas para aumentar a legibilidade do código. Use tuplas para dados temporários: tuplas são ideais para valores temporários ou resultados rápidos, mas não devem ser usadas como tipos permanentes ou complexos. Conclusão As tuplas no C# evoluíram significativamente ao longo das versões do .NET, oferecendo uma forma mais simples e eficiente de agrupar múltiplos valores sem a necessidade de classes ou structs. Elas são ideais para cenários que exigem um agrupamento rápido e simples de dados temporários. Se usadas corretamente, as tuplas podem melhorar significativamente a legibilidade e performance do seu código, especialmente quando se trata de retornos múltiplos ou manipulação de dados temporários.

Paginação de dados em .NET: um guia completo

Quando precisamos lidar com um grande volume de dados a paginação se torna essencial, principalmente quando esses dados precisam ser apresentados ao usuário de forma eficiente e escalável. Sem uma implementação adequada de paginação, os sistemas podem enfrentar sérios problemas de desempenho e de experiência do usuário, especialmente ao carregar listas de milhares ou milhões de registros. Importância da paginação A paginação é fundamental para: Otimizar o desempenho: carregar um grande volume de dados de uma vez pode causar lentidão na aplicação e sobrecarga do servidor. A paginação permite carregar apenas uma parte dos dados por vez, o que melhora o desempenho e reduz a latência. Melhorar a experiência do usuário: a navegação por páginas proporciona uma interação mais fluida, sem necessidade de esperar por um grande conjunto de dados para ser carregado. Os usuários podem acessar os dados de forma mais rápida e sem esperar muito tempo. Reduzir o uso de recursos: ao carregar somente uma parte dos dados de cada vez, o uso de memória e processamento é significativamente reduzido, o que pode ser crítico em ambientes com muitos usuários simultâneos. Abordagens de paginação em .NET Neste artigo vamos ver três abordagens principais para implementar a paginação em uma aplicação .NET: paginação em banco de dados, paginação com Entity Framework e paginação manual em listas. Paginação em banco de dados com SQL A primeira abordagem de paginação é realizada diretamente no banco de dados. Utilizando comandos SQL, é possível carregar uma parte dos dados com base em parâmetros como “OFFSET” e “FETCH NEXT”, que permitem dividir os dados em páginas e evitar a sobrecarga de memória. public async Task GetPagedProductsAsync(int pageNumber, int pageSize) { var sql = @” SELECT * FROM Products ORDER BY ProductID OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY; “; var offset = (pageNumber – 1) * pageSize; var products = await _dbConnection.QueryAsync(sql, new { Offset = offset, PageSize = pageSize }); } Aqui, o parâmetro “@PageNumber” representa o número da página que você deseja consultar e “@PageSize” representa o número de registros que serão retornados por página. A cláusula “OFFSET” é usada para determinar quantos registros serão ignorados antes de começar a retornar os resultados da consulta, com base no número da página e no tamanho da página. Usado um exemplo repassando a query com o Dapper, se você receber o “pageNumber = 1” e o “pageSize = 20”, a consulta trará os primeiros 20 registros. Para “pageNumber = 2”, a consulta irá pular os primeiros 20 registros e trará os 20 seguintes, ou seja, do 21º ao 40º registro. A cláusula “FETCH NEXT” é responsável por limitar o número de registros que serão retornados na consulta, baseando-se em “@PageSize”. Paginação com Entity Framework Quando utilizamos o Entity Framework (EF) para implementar paginação, a abordagem é mais abstrata e orientada a objetos em comparação com a paginação enviando diretamente o SQL para o banco de dados. Em vez de usar comandos SQL como “OFFSET” e “FETCH NEXT”, o EF fornece os métodos “Skip()” e “Take()” para realizar a paginação de maneira mais simples e intuitiva. public async Task GetPagedProductsAsync() { var pageNumber = 1; var pageSize = 15; var query = _context.Products.AsQueryable(); var items = await query .Skip((pageNumber – 1) * pageSize ) .Take(pageSize) .ToListAsync(); return items; } No exemplo apresentado, a consulta começa criando um “IQueryable” a partir da coleção de produtos (_context.Products), permitindo que você construa e manipule a consulta de maneira eficiente, sem realizar a execução imediata no banco de dados.  O método “Skip()” é utilizado para ignorar os registros anteriores à página solicitada, ou seja, ele “pula” os registros de páginas anteriores. A fórmula usada para calcular o número de registros a serem ignorados é “(pageNumber – 1) * pageSize”, onde “pageNumber” é o número da página e “pageSize” é o número de registros por página.  Já o método “Take()” limita a quantidade de registros retornados para o número de itens da página atual (definido por “pageSize”). Paginação manual em listas Quando os dados já estão carregados em memória, pode ser necessário implementar a paginação manual em listas, sem o uso de banco de dados. Isso ocorre frequentemente quando você tem uma coleção de dados já disponível, como uma lista em memória, e deseja dividi-la em páginas para melhorar a apresentação ou a navegação.  Esse método é útil quando os dados não são tão grandes e podem ser manipulados diretamente na memória, evitando a necessidade de consultas adicionais ao banco de dados. public Products GetPagedProducts(List products, int pageNumber, int pageSize) { var totalItems = products.Count; var items = products.Skip((pageNumber – 1) * pageSize) .Take(pageSize) .ToList(); var totalPages = (int)Math.Ceiling((double)totalItems / pageSize); return new Products { Items = items, CurrentPage = pageNumber }; } No exemplo fornecido, a função “GetPagedProducts” recebe uma lista de produtos (List<Product> products), o número da página (pageNumber) e o tamanho da página (pageSize). Primeiro, o total de itens na lista é calculado usando o método “Count()”. Vamos usar esse valor para calcular a quantidade de páginas mais à frente.  Em seguida, “Skip()” é usado para pular os itens das páginas anteriores, calculando a quantidade de itens a serem ignorados com base no número da página e no tamanho da página: “(pageNumber – 1) * pageSize”. Após isso, o método “Take()” é utilizado para retornar os itens correspondentes à página atual, ou seja, os registros que serão exibidos naquela página. O número total de páginas é calculado dividindo o total de itens (totalItems) pela quantidade de itens por página (pageSize) e arredondando para cima com “Math.Ceiling()”, para garantir que uma página extra seja retornada quando o total de registros não for divisível perfeitamente pelo tamanho da página. Por fim, o método retorna uma lista de “Product”, que contém os itens da página atual, e o número da página atual + 1 para que na próxima requisição seja enviada a próxima página. Isso proporciona uma estrutura organizada para a paginação. Criando uma API paginada em ASP.NET Core Agora vamos demonstrar como criar uma API paginada utilizando

Extension Methods em C#: como estender classes e otimizar seu código

Extension Methods são uma funcionalidade do C# que permite expandir o comportamento de classes já existentes sem modificá-las diretamente ou usar herança. Essa técnica se mostra especialmente útil quando o código-fonte original não está disponível ou quando se busca preservar uma arquitetura de código limpa e desacoplada. Neste artigo você aprenderá o que são extension methods, como criá-los, utilizá-los de maneira eficaz e aplicá-los em casos práticos. O que são extension methods? Extension Methods são métodos estáticos que permitem “estender” a funcionalidade de tipos existentes. Eles podem ser aplicados tanto a tipos definidos pelo usuário quanto a tipos internos do .NET, como “string”, “List<T>”, “DateTime”, entre outros. A grande vantagem dos extension methods é que eles são invocados como se fossem métodos de instância, melhorando a legibilidade do código. Como criar um extension method Regras básicas Para criar um extension method, você precisa seguir algumas regras: Ele deve estar em uma classe estática. O próprio método também deve ser estático. O primeiro parâmetro do método deve usar a palavra-chave “this”, seguida do tipo que você deseja estender. Estrutura básica Vamos adicionar o método “InverterString” para inverter uma string: public static class ExtensoesDeString { public static string InverterString(this string valor) { return new string(valor.ToCharArray().Reverse().ToArray()); } } Aqui, o “this string valor” indica que estamos estendendo a classe “string”. Com isso, qualquer instância de “string” pode chamar esse método diretamente. Utilizando o método criado Depois de criar um extension method, você pode utilizá-lo de forma muito simples: string texto = “NextWaveEducation”; Console.WriteLine(texto.InverterString()); Note que o método “InverterString()” aparece no IntelliSense (Ctrl + Espaço) como se fosse um método nativo da classe string, tornando o código mais limpo e intuitivo. Boas práticas com extension methods Quando usar Quando você precisa adicionar funcionalidades sem modificar a classe original.  Para encapsular códigos repetitivos e melhorar a legibilidade. Para fornecer funções auxiliares que fazem sentido dentro do contexto de um tipo. Quando evitar Evite criar extension methods para funcionalidades triviais que poderiam ser implementadas diretamente na classe original. Evite extension methods que alterem o estado interno do objeto estendido, pois isso pode tornar o comportamento inesperado. Nomeação clara Use nomes intuitivos que sigam as convenções do C#. Certifique-se de que o nome do método indica claramente sua funcionalidade. Não crie métodos com nomes que possam causar confusão com métodos nativos. Caso de uso real Aqui está um exemplo que verifica se uma data é um fim de semana: public static class ExtensoesDeData { public static bool EhFimDeSemana(this DateTime data) => data.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday; } Acelere a sua carreira conosco! Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET Start: Clique aqui Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET Expert: Clique aqui Conclusão Extension Methods são uma ferramenta valiosa para melhorar a produtividade e a organização do código em C#. Eles permitem que você adicione funcionalidades de maneira elegante e eficiente, tornando o código mais limpo e legível. Pratique criando seus próprios extension methods e aplique-os em seus projetos para ver os benefícios em ação.

Utilizando indexadores em classes C#

Em C#, indexadores são recursos que permitem que objetos de classes sejam acessados de maneira semelhante a arrays ou coleções, utilizando a sintaxe de índices (os colchetes []). Essa funcionalidade torna o código mais intuitivo e legível, oferecendo uma forma concisa de manipular dados armazenados em objetos de classes personalizadas. Neste artigo vamos explorar o conceito de indexadores, como eles são declarados e implementados, e como podem ser usados para criar coleções personalizadas ou facilitar o acesso a dados. O que são indexadores? Indexadores são membros de uma classe ou struct que permitem acessar elementos de um objeto usando a sintaxe de índice. Embora a sintaxe de índices seja comumente associada a arrays e coleções, indexadores permitem que você ofereça esse comportamento em suas próprias classes. A declaração de um indexador em C# envolve o uso da palavra-chave “this”, seguida de um ou mais parâmetros entre colchetes, representando o índice. Esses parâmetros determinam como o acesso aos elementos é feito. Como declarar e implementar indexadores A sintaxe básica para declarar um indexador é a seguinte: public tipo this[tipos índices] { get { … } set { … } } Aqui, tipo é o “tipo” de dado que o indexador irá acessar (ou seja, o tipo do valor que será retornado ou atribuído), e “tipos índices” são os parâmetros que o indexador aceita (frequentemente usados para representar um índice ou chave). Para entendermos melhor, vamos supor que você queira criar uma classe “MinhaColecao” que armazena uma lista de inteiros. Se fizermos da forma tradicional para acessar as informações de dados, poderíamos implementar algo assim: public class MinhaColecao { private int[] dados; public MinhaColecao(int tamanho) { dados = new int[tamanho]; } public int GetValor(int index) { return dados[index]; } public void SetValor(int index, int valor) { dados[index] = valor; } } O acesso aos elementos seria feito chamando métodos como “GetValor” e “SetValor”: var colecao = new MinhaColecao(5); colecao.SetValor(0, 10); colecao.SetValor(1, 20); Console.WriteLine(colecao.GetValor(0)); Console.WriteLine(colecao.GetValor(1)); Embora funcional, essa abordagem exige a utilização explícita de métodos para acessar e modificar os dados, o que pode tornar o código mais verboso. Mas usando um indexador, podemos fazer da seguinte forma: public class MinhaColecao { private int[] dados; public MinhaColecao(int tamanho) { dados = new int[tamanho]; } public int this[int index] { get { return dados[index]; } set { dados[index] = value; } } } Com o indexador, o acesso aos elementos fica mais intuitivo e direto: var colecao = new MinhaColecao(5); colecao[0] = 10; colecao[1] = 20; Console.WriteLine(colecao[0]); Console.WriteLine(colecao[1]); Perceba que a sintaxe de índices (colecao[0]) torna o código mais limpo e fácil de entender, eliminando a necessidade de métodos adicionais para acesso aos dados. O indexador age como uma ponte que simplifica a interação com os elementos da classe. Boas práticas de design para indexadores Evitar Ambiguidade: Se o seu indexador tiver múltiplos parâmetros ou tipos de índice diferentes, tome cuidado para não criar ambiguidade. Isso pode tornar o código confuso e difícil de manter. public string this[int index, string chave] { get; set; } Esse tipo de indexador pode causar confusão se você não tiver uma necessidade clara para dois parâmetros diferentes. Em vez disso, considere usar métodos ou propriedades para separar as responsabilidades. Validação: Sempre que possível, valide os índices ou as chaves antes de acessar os dados. Isso ajuda a evitar exceções inesperadas no seu código. public int this[int index] { get { if (index < 0 || index >= dados.Length) { throw new IndexOutOfRangeException(“Índice fora dos limites da coleção.”); } return dados[index]; } set { if (index < 0 || index >= dados.Length) { throw new IndexOutOfRangeException(“Índice fora dos limites da coleção.”); } if (value < 0) { throw new ArgumentException("O valor não pode ser negativo."); } dados[index] = value; } } Consistência: Certifique-se de que as operações de “get” e “set” sigam a mesma lógica de acesso. Isso garante que o comportamento do indexador seja consistente e previsível. Desempenho: Em casos em que o acesso aos dados é frequentíssimo, tome cuidado com o impacto de validações ou transformações dentro do indexador. Tente manter o código de “get” e “set” o mais simples possível para não impactar a performance. Imutabilidade: Se o seu objeto deve ser imutável, lembre-se de que um indexador de leitura (get) sem set pode ser uma boa escolha. Isso garante que os dados não sejam alterados após a criação do objeto. Exemplo prático: usando indexadores com dicionário personalizado Agora, vamos ver um exemplo prático de como usar um indexador para criar um dicionário personalizado, onde as chaves são strings e os valores são inteiros. public class MeuDicionario { private Dictionary dados = new Dictionary(); public int this[string chave] { get { if (!dados.ContainsKey(chave)) throw new KeyNotFoundException("Chave não encontrada."); return dados[chave]; } set { dados[chave] = value; } } } Usando o “MeuDicionario” var dicionario = new MeuDicionario(); dicionario["um"] = 1; dicionario["dois"] = 2; Console.WriteLine(dicionario["um"]); Console.WriteLine(dicionario["dois"]); Neste exemplo, a classe “MeuDicionario” oferece acesso a dados por meio de chaves de tipo string, permitindo armazenar e recuperar valores de maneira simples e eficiente. Acelere a sua carreira conosco! Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET Start: Clique aqui Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET Expert: Clique aqui Conclusão Os indexadores em C# oferecem uma maneira prática e intuitiva de acessar dados em objetos, similar ao acesso a elementos de arrays ou coleções. Eles permitem criar classes que encapsulam dados de maneira eficiente, mantendo o código limpo e legível. Ao utilizar indexadores, é importante aplicar boas práticas, como validação de índices e evitar ambiguidades, para garantir que o design do seu código seja fácil de manter.

Índices e intervalos em arrays no C#

Os arrays são estruturas fundamentais em C#, permitindo armazenar e manipular coleções de elementos do mesmo tipo. A partir do C# 8.0, novas funcionalidades foram introduzidas para tornar o trabalho com arrays ainda mais eficiente. Neste artigo vamos explorar desde os índices tradicionais até as últimas novidades como índices negativos com o operador “^” e intervalos “(..)” para criar subarrays. Índices tradicionais em arrays Nos arrays, os índices são utilizados para acessar elementos individuais. Eles começam do zero, ou seja, o primeiro elemento está no índice 0, o segundo no índice 1, e assim por diante. int[] numeros = { 10, 20, 30, 40, 50 }; Console.WriteLine(numeros[0]); Console.WriteLine(numeros[2]); Se você tentar acessar um índice fora dos limites do array (numeros[5]), uma exceção “IndexOutOfRangeException” será lançada. Índices negativos com o operador ^ Com o C# 8.0, foi introduzido o operador “^” para trabalhar com índices relativos ao final do array. Isso simplifica o acesso ao último elemento ou a elementos próximos ao fim. int[] numeros = { 10, 20, 30, 40, 50 }; Console.WriteLine(numeros[^1]); Console.WriteLine(numeros[^2]); Aqui, o número após o operador “^” indica a posição a ser acessada de trás para frente no array. No exemplo acima, o console exibirá os valores 50 (último elemento) e 40 (penúltimo elemento). Intervalos com o operador .. O operador “..” permite criar subarrays (fatias ou slices) de maneira concisa. Ele utiliza dois índices, um inicial e um final, separados por “..”. Regras para usar o operador .. Ao definir um intervalo como [inicial..final], você especifica onde o intervalo começa e termina. int[] numeros = { 10, 20, 30, 40, 50 }; int[] fatia1 = numeros[1..3]; Console.WriteLine(string.Join(“, “, fatia1)); Nesse exemplo, a fatia inclui os elementos “numeros[1]” (20) e “numeros[2]” (30), mas exclui o elemento “numeros[3]”. Se você omitir o índice inicial (..final), o intervalo começa no primeiro elemento do array e vai até o índice especificado (não incluído). int[] fatia2 = numeros[..3]; Console.WriteLine(string.Join(“, “, fatia2)); O intervalo “[..3]” inclui os elementos “numeros[0]”, “numeros[1]” e “numeros[2]”. Se você omitir o índice final (inicial..), o intervalo vai do índice inicial especificado até o último elemento do array. int[] fatia3 = numeros[2..]; Console.WriteLine(string.Join(“, “, fatia3)); Nesse caso, o intervalo “[2..]” inclui todos os elementos a partir de “numeros[2]” até o final do array. Se você omitir tanto o índice inicial quanto o final (..), o intervalo abrange o array inteiro. Essa sintaxe equivale a criar uma cópia completa do array. int[] fatiaCompleta = numeros[..]; Console.WriteLine(string.Join(“, “, fatiaCompleta)); Combinação com o operador ^ O operador “^” pode ser combinado com “..” para criar intervalos relativos ao final do array. Isso é útil para trabalhar com os últimos elementos de uma coleção. int[] ultimos = numeros[^3..]; Console.WriteLine(string.Join(“, “, ultimos)); O [ ^3.. ] começa no terceiro elemento a partir do final e vai até o final do array. A saída será 30, 40, 50. int[] semOsUltimos = numeros[..^2]; Console.WriteLine(string.Join(“, “, semOsUltimos)); O [ ..^2 ] vai do início do array até dois elementos antes do final. O resultado será 10, 20, 30. Combinando intervalos com métodos de extensão Uma outra possibilidade do C# é a de combinar intervalos com métodos de extensão como “Where”, “Select”, e outros disponíveis no LINQ. Isso permite criar filtros e transformações dinâmicas diretamente sobre fatias de arrays. Você pode usar o operador “..” para criar um subarray e aplicar um filtro dinâmico com o método “Where”: int[] numeros = { 10, 15, 20, 25, 30 }; var pares = numeros[1..4].Where(n => n % 2 == 0); Console.WriteLine(string.Join(“, “, pares)); O intervalo “[1..4]” seleciona os elementos do índice 1 ao índice 3  (15, 20, 25). Após isso, o método “Where” filtra os elementos que são pares. A saída resulta em 20. Além de filtrar, você pode transformar os valores de um intervalo utilizando o método “Select”: int[] numeros = { 10, 15, 20, 25, 30 }; var dobrados = numeros[2..].Select(n => n * 2); Console.WriteLine(string.Join(“, “, dobrados)); Neste exemplo, o intervalo “[2..]” seleciona todos os elementos a partir do índice 2 (20, 25, 30). O método “Select” aplica a transformação “n * 2” a cada elemento do intervalo. O resultado será uma nova lista com os números 40, 50, 60 armazenados na variável “dobrados”. Acelere a sua carreira conosco! Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET Start: Clique aqui Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET Expert: Clique aqui Conclusão Índices e intervalos negativos em C# oferecem novas possibilidades para manipular arrays e coleções de forma simples e eficiente. Esses recursos tornam o código mais expressivo, eliminando a necessidade de cálculos manuais de índice. Além disso, intervalos e índices negativos podem ser combinados com outros recursos modernos de C# para criar soluções robustas e claras. Explore essas funcionalidades em seus projetos e veja como elas podem simplificar suas implementações!

Integre suas aplicações .NET com o DeepSeek

Recentemente, a start-up chinesa DeepSeek tem ganhado destaque no campo da inteligência artificial com seu revolucionário chatbot de IA generativa. O modelo da DeepSeek alcançou um nível de desempenho comparável aos de gigantes como OpenAI e Anthropic, mas com um custo significativamente mais baixo. Neste artigo, iremos explorar como integrar a API DeepSeek em uma aplicação .NET, mostrando passo a passo como configurar, enviar e processar mensagens para criar uma experiência de bate-papo inteligente e fluída. Se você deseja adicionar funcionalidades de conversação inteligente ao seu projeto, este guia prático é o ponto de partida perfeito. Criando uma conta no DeepSeek Acesse o site oficial do DeepSeek. Preencha o formulário com seu e-mail e senha. Após criar sua conta, faça login e vá até a seção API Keys no painel de controle e clique em “Create new API Key”, copie e guarde a chave gerada. Configurando a API Key Agora, no código que vamos escrever, você deve inserir sua chave de API gerada no DeepSeek. No exemplo abaixo, a chave de API é armazenada diretamente na variável apiKey. Em aplicações reais, evite armazenar a chave diretamente no código fonte. Em vez disso, você pode armazená-la em uma variável de ambiente ou em um arquivo de configuração. public class Program { private static readonly string apiKey = “Sua-chave”; private static readonly string baseUrl = “https://api.deepseek.com/chat/completions”; public static async Task Main(string[] args) { //nosso código } } Função principal para envio de mensagens Na “Main”, começamos inicializando a lista de mensagens e configuramos o sistema para enviar as mensagens ao modelo de chat da API DeepSeek. A API requer um formato específico de dados para a comunicação. Aqui, o código define um ponto de entrada para a conversa, onde o sistema envia uma mensagem de saudação inicial ao DeepSeek: Console.WriteLine(“Iniciando conversa com o DeepSeek…”); var messages = new List(); messages.Add(new { role = “system”, content = “Você é um assistente inteligente e amigável.” } Esse código simula uma conversa onde o “role” define quem está enviando a mensagem. Já o “content” armazena qual o comportamento do assistente. Leitura da entrada do usuário e envio de solicitações Agora vamos criar um  “while” que vai continuar pedindo mensagens ao usuário e as envia para o DeepSeek. Caso o usuário queira parar o “while” ele teve enviar uma mensagem vazia: while (true) { //…código do processamento aqui } Os próximos blocos de código devem ser inseridos dentro do while(true). Console.Write(“Você: “); string userMessage = Console.ReadLine(); if (string.IsNullOrEmpty(userMessage)) break; Após escrever a mensagem, nós vamos adicionar ela na lista de mensagens: messages.Add(new { role = “user”, content = userMessage }); Em seguida, vamos montar o corpo da nossa requisição para enviar a solicitação ao DeepSeek. Essa requisição vai conter todas as mensagens trocadas até aquele momento (usuário e assistente): var requestData = new { model = “deepseek-chat”, messages = messages.ToArray(), stream = false }; Enviando a solicitação HTTP para a API Agora vamos usar a classe “HttpClient” para enviar uma requisição POST: using var httpClient = new HttpClient(); string jsonRequest = JsonSerializer.Serialize(requestData); var content = new StringContent(jsonRequest, Encoding.UTF8, “application/json”); httpClient.DefaultRequestHeaders.Add(“Authorization”, $”Bearer {apiKey}”); var response = await httpClient.PostAsync(baseUrl, content); Aqui, a chave de API é passada no cabeçalho da requisição através do cabeçalho “Authorization”. Tratando a resposta da API Após isso, é necessário analisar a resposta da API e extrair a resposta do modelo. A API DeepSeek retorna a resposta em um formato JSON, e usamos “JsonDocument” para processá-la: var responseBody = await response.Content.ReadAsStringAsync(); using JsonDocument jsonDoc = JsonDocument.Parse(responseBody); string contentText = jsonDoc .RootElement .GetProperty(“choices”)[0] .GetProperty(“message”) .GetProperty(“content”) .GetString() O resultado será apenas a resposta do modelo sem os demais dados. Apresentando as informações e armazenando a nova mensagem Por fim, podemos apresentar a resposta do modelo na tela e adicionar a mensagem recebida na lista de mensagens e continuando no ‘while’: Console.WriteLine($”DeepSeek: {contentText }”); messages.Add(new { role = “assistant”, content = contentText }); Na imagem abaixo podemos ver uma demonstração da aplicação em funcionamento: Acelere a sua carreira conosco! Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET Start: Clique aqui Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET Expert: Clique aqui Conclusão Neste artigo, nós vimos como integrar com a API DeepSeek em um aplicativo .NET para criar uma aplicação de bate-papo simples. Com base nesse código, você pode expandir a funcionalidade para criar interfaces gráficas, armazenar o histórico de mensagens em um banco de dados, ou até mesmo integrar com outras APIs ou sistemas.

Crie um assistente virtual em .NET com a API da OpenAI

Com a crescente demanda por soluções inteligentes, integrar assistentes de IA em aplicações tornou-se uma oportunidade valiosa para desenvolvedores. Para isso, a OpenAI ampliou suas capacidades com a API Assistants, permitindo a criação de assistentes personalizados com funcionalidades avançadas. Neste artigo, exploraremos como utilizar essa API, desde a configuração inicial até exemplos práticos. Quais as diferenças entre a API do ChatGPT e a API Assistants? A API Assistants e a API tradicional do ChatGPT compartilham a base tecnológica dos modelos avançados da OpenAI, mas possuem abordagens distintas quanto à funcionalidade e ao uso: Personalização e instruções específicas: ChatGPT: possui um foco mais direto em responder perguntas e participar de conversas com base nas mensagens enviadas. Assistants: permite configurar assistentes com instruções específicas para definir a personalidade e os limites do assistente, ajustando-o para diferentes casos de uso. Threads persistentes: ChatGPT: não armazena o histórico de conversas diretamente. Assistants: trabalha com Threads que armazenam o histórico de mensagens, possibilitando a continuidade das interações mesmo após múltiplas consultas. Manipulação de arquivos: ChatGPT: não interage diretamente com arquivos. Assistants: suporta o acesso e manipulação de arquivos em diversos formatos, além de citar arquivos e criar novos durante a execução de tarefas. Configuração inicial Para fins didáticos, utilizaremos aqui uma aplicação do tipo Console. Então, para começar, vamos organizar a estrutura básica da nossa aplicação de forma clara: public class Program { private static readonly string apiKey = “”; // Substitua com sua chave de API private static readonly string apiUrl = “https://api.openai.com/v1″; private static string assistantId; private static string threadId; public static async Task Main(string[] args) { try { //Implementação do nosso código. } catch (Exception ex) { Console.WriteLine($”Erro geral: {ex.Message}”); } } private static async Task CriarAssistente() { } private static async Task CriarThread() { } private static async Task EnviarMensagem(string assistantId, string threadId, string Mensagem) { } private static async Task BuscarMensagens(string threadId) { } } Dentro da classe “Program” nós criamos quatro variáveis. Na variável “apiKey” deve ser armazenada a chave de API da OpenAI. Caso tenha dúvidas de como conseguir essa chave, você pode ver o passo a passo neste tutorial.  A URL base (apiUrl) é fixa e aponta para o endpoint de interações com assistentes. A variável “assistantId” vai armazenar o id da nossa assistente e a “threadId” vai armazenar o id da thead que vamos criar. O método principal será a “Main”, responsável por iniciar a execução do sistema e controlar o fluxo principal da interação com a API da OpenAI. Além dela, contaremos com quatro métodos que desempenham funções específicas: CriarAssistente: Este método será responsável por enviar uma requisição para a criação de um assistente virtual. Ele configurará parâmetros como instruções iniciais e o modelo que será utilizado. CriarThread: Após a criação do assistente, será necessário criar uma thread associada a ele, onde as mensagens e respostas serão organizadas. Este método realiza essa tarefa. EnviarMensagem: Uma vez que a thread estiver configurada, este método permitirá enviar mensagens para o assistente virtual, incluindo instruções e dados adicionais conforme necessário. BuscarMensagens: Este método será responsável por recuperar as mensagens da thread, exibindo as respostas geradas pelo assistente. Criando o assistente virtual A primeira etapa para interagir com a API do OpenAI é criar um assistente. Vamos definir algumas configurações iniciais no método “CriarAssistente”: private static async Task CriarAssistente() { using var client = new HttpClient(); var data = new { instructions = “Você é um professor de matemática que auxilia seus alunos.”, name = “Professor”, tools = new List { new { type = “file_search” }, new { type = “code_interpreter” } }, model = “gpt-4-turbo” }; //…continua } Aqui nós criamos uma instância de “HttpClient” para realizarmos as requisições. Após isso, montamos o objeto da nossa requisição que vai conter a instrução do que nossa assistente deve fazer, o seu nome, uma lista de ferramentas que o assistente pode usar como pesquisa de arquivos e interpretação de código. E por fim, o modelo que usaremos. Ainda dentro do método “CriarAssistente”, vamos montar a nossa requisição e envia-la: var request = new HttpRequestMessage(HttpMethod.Post, $”{apiUrl}/assistants”) { Content = new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, “application/json”) }; request.Headers.Add(“Authorization”, $”Bearer {apiKey}”); request.Headers.Add(“OpenAI-Beta”, “assistants=v2”); var response = await client.SendAsync(request); Após realizar o envio da requisição, vamos ler a informação que foi retornada: var result = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(result); return doc.RootElement.GetProperty(“id”).GetString(); No código acima, após realizarmos a leitura da nossa resposta, vamos armazená-la em “result”. Após isso, vamos transformar o valor recebido em um JSON para permitir o acesso estruturado aos seus dados. O código então vai navegar até a raiz do documento (RootElement) e extrair o valor associado à propriedade “id”, convertendo-o em uma string com “GetString()”.  Com isso, conseguimos capturar o id da nossa assistente. Criando a thread de conversa Após criar o assistente, precisamos de uma “thread” (ou conversa) onde as interações acontecerão. private static async Task CriarThread() { using var client = new HttpClient(); var data = new { metadata = new { project = “AulasMatematica” } }; //Aqui o objeto “data” vai conter as informações do projeto que você está criando. var request = new HttpRequestMessage(HttpMethod.Post, $”{apiUrl}/threads”) { Content = new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, “application/json”) }; request.Headers.Add(“Authorization”, $”Bearer {apiKey}”); request.Headers.Add(“OpenAI-Beta”, “assistants=v2”); var response = await client.SendAsync(request); var result = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(result); return doc.RootElement.GetProperty(“id”).GetString(); } O código acima vai seguir o mesmo fluxo da criação da assistente, retornando o id da thread que foi criada. Enviando mensagens para o assistente Agora que temos o assistente e a thread criados, podemos enviar mensagens para o assistente da seguinte forma: private static async Task SendMessageAsync(string assistantId, string threadId, string mensagem) { using var client = new HttpClient(); var data = new { assistant_id = assistantId, instructions = “Responda apenas às perguntas referentes a matemática. Caso seja falado sobre outros assuntos, responda educadamente para voltar para assuntos relacionados a matemática.”, additional_messages = new[] { new { role = “user”, content = mensagem} } }; //…continua } Aqui, a variável “data” guarda o “assistantId” do assistente que

Domine o yield em C#: iteração sob demanda e eficiência de memória

Em aplicações que lidam com grandes volumes de dados, pode ser necessário começar a retornar resultados antes que todo o processamento seja concluído. Isso é especialmente útil para evitar o consumo excessivo de memória e melhorar o desempenho da aplicação. No C#, a palavra-chave “yield” é uma ferramenta bastante útil para lidar com essa necessidade, permitindo a criação de iteradores que geram valores sob demanda. Neste artigo exploraremos como “yield return” e “yield break” funcionam, sua aplicação prática e os benefícios no gerenciamento de coleções grandes. O que é yield? Em C#, “yield” é usado dentro de métodos iteradores para retornar elementos um por um, sem precisar armazenar todos os itens na memória. A palavra-chave “yield return” permite que um método produza uma sequência de valores sob demanda, enquanto “yield break” encerra a iteração antecipadamente. Como yield funciona internamente em métodos iteradores Quando um método contém “yield return”, o compilador gera automaticamente um iterador que preserva o estado entre as chamadas subsequentes. Isso significa que os valores são calculados sob demanda, permitindo uma iteração eficiente em memória. yield return Para entendermos melhor vamos ver um exemplo prático: foreach (var numero in GeradorNumeros(5)) { Console.WriteLine(numero); } IEnumerable GeradorNumeros(int quantidade) { for (int i = 1; i

Como implementar upload de arquivos no ASP.NET MVC

Quando desenvolvemos aplicações web, é comum precisar de funcionalidades para adicionar imagens ou arquivos, como fotos de perfil de usuários, imagens de produtos, documentos anexados e muito mais. Implementar essa funcionalidade de forma eficiente e segura é essencial para uma boa experiência do usuário e para a integridade do sistema. Neste artigo vamos explorar como configurar o upload de arquivos em uma aplicação ASP.NET MVC, abordando cenários práticos, como salvar arquivos no sistema de arquivos local, armazená-los em um banco de dados. Configurando o controlador para upload Crie um controlador chamado “FileUploadController” com as seguintes ações: public class FileUploadController : Controller { public IActionResult Index() { return View(); }; [HttpPost] public async Task Upload(IFormFile file) { //código que recebe o arquivo. } } No código acima criamos duas actions no controlador “FileUploadController”. A primeira, “Index”, é responsável por exibir a tela com o formulário para envio de arquivos, onde o usuário poderá selecionar o arquivo desejado.  Já a segunda, “Upload”, é a action que recebe o arquivo enviado pelo formulário. Para isso nós usamos a interface “IFormFile”, que é responsável por encapsular os dados do arquivo enviados pelo cliente e permitir o acesso diretamente no lado do servidor. Mais adiante vamos detalhar a implementação da lógica na action “Upload” para processar o arquivo enviado pelo usuário. Esse método seguirá etapas para validar o arquivo e salvar o conteúdo de forma segura. Criando a view com o formulário de upload Agora vamos criar a view com o nome “Index.cshtml” com o nosso formulário: Escolha o arquivo: Enviar No codigo acima, nós criamos um formulário informando que ele deve ser enviado para action “Upload” que está na controller “FileUpload”. Logo abaixo, vamos adicionar as mensagens que serão retornadas em caso de erro ou sucesso: @if (TempData[“Message”] != null) { @TempData[“Message”] } @foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors)) { @error.ErrorMessage } Validando o se o arquivo é válido A primeira validação é para saber se foi enviado um arquivo válido. if (file == null || file.Length == 0) { ModelState.AddModelError(“”, “Selecione um arquivo válido.”); return View(“Index”); } Se o arquivo recebido for nulo ou tiver tamanho zero, o sistema exibirá uma mensagem de erro informando o problema e redirecionará o usuário de volta à tela inicial (Index). Isso evita processar uploads inválidos. Validação de extensão Agora vamos adicionar uma validação para aceitar apenas arquivos com determinadas extensões. var allowedExtensions = new[] { “.jpg”, “.png”, “.pdf” }; var extension = Path.GetExtension(file.FileName).ToLower(); if (!allowedExtensions.Contains(extension)) { ModelState.AddModelError(“”, “Formato de arquivo não permitido.”); return View(“Index”); } Nesta etapa, permitimos apenas arquivos com as extensões .jpg, .png e .pdf. Qualquer outro formato será rejeitado com uma mensagem de erro. Isso protege a aplicação de uploads de arquivos maliciosos. Validação de tamanho máximo Além disso, teremos uma validação tamanho máximo do arquivo. if (file.Length > 5 * 1024 * 1024) { ModelState.AddModelError(“”, “O arquivo não pode exceder 5 MB.”); return View(“Index”); } Isso vai evitar sobrecarga no servidor e uploads indesejados, impomos um limite de tamanho (5 MB). Arquivos maiores serão rejeitados com uma mensagem adequada. Armazenando o arquivo na aplicação Agora que adicionamos as validações, vamos armazenar o arquivo na aplicação, garantindo que a pasta de destino exista antes de salvar o arquivo. Primeiro, definimos o diretório onde os arquivos serão armazenados: var uploadDirectory = Path.Combine(Directory.GetCurrentDirectory(), “wwwroot/uploads”); Aqui, indicamos que os arquivos serão salvos na pasta “wwwroot/uploads”. Caso essa pasta não exista, podemos criá-la da seguinte forma: if (!Directory.Exists(uploadDirectory)) { Directory.CreateDirectory(uploadDirectory); } Essa verificação evita erros no caso de a pasta ainda não estar criada no servidor ou em ambientes de desenvolvimento. Em seguida, definimos o caminho completo para o arquivo: var filePath = Path.Combine(uploadDirectory, file.FileName); Agora, usamos um FileStream para gravar os dados do arquivo no local especificado: using (var stream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(stream); } Essa operação realiza a gravação de forma assíncrona, permitindo que a aplicação permaneça responsiva mesmo durante o upload de arquivos. //Implementar lógica de gravação do nome do arquivo no banco. TempData[“Message”] = “Arquivo enviado com sucesso!”; return RedirectToAction(“Index”); Após salvar o arquivo, você pode implementar um procedimento para salvar as informações do arquivo (nome, extensão …) no banco de dados. Após isso, armazenamos uma mensagem de sucesso em “TempData” e redirecionamos o usuário de volta à tela inicial. Essa mensagem será exibida na view para confirmar que o upload foi concluído. Salvando em banco de dados Para salvar uma imagem diretamente no banco de dados você pode criar uma entidade “FileRecord” para armazenar as informações do arquivo. Nesta entidade a propriedade “Data” armazena os dados reais do arquivo em formato binário (array de bytes). É nessa propriedade que os dados do arquivo serão salvos. public class FileRecord { public int Id { get; set; } public string FileName { get; set; } public string ContentType { get; set; } public byte[] Data { get; set; } } Agora podemos reaproveitar a action que “Upload” que criamos anteriormente para processar o arquivo recebido. public async Task Upload(IFormFile file) { //Adicione as validações do arquivo recebido. using var memoryStream = new MemoryStream(); await file.CopyToAsync(memoryStream); var fileRecord = new FileRecord { FileName = file.FileName, ContentType = file.ContentType, Data = memoryStream.ToArray() }; context.FileRecords.Add(fileRecord); await context.SaveChangesAsync(); return RedirectToAction(“Index”); } Primeiro, criamos uma variável do tipo “MemoryStream”, que serve para armazenar temporariamente os dados do arquivo em memória.  Em seguida, utilizamos o método “CopyToAsync” do arquivo enviado (file), que copia os dados do arquivo diretamente para o “memoryStream”. Após obter os dados do arquivo, criamos uma entidade do tipo “FileRecord”, que representa a estrutura do arquivo que será salvo no banco de dados. Com a entidade “fileRecord” criada e preenchida, nós a adicionamos ao banco de dados usando o método “Add” no contexto da aplicação (context). Concluímos redirecionando o fluxo para a view “Index”, proporcionando uma experiência contínua e amigável. Observação: Neste exemplo, não incluímos validações específicas para o arquivo recebido. Entretanto, em aplicações reais, é recomendável implementar validações para garantir segurança e consistência. Dicas de segurança Validação do tipo de arquivo:

Serialização avançada com System.Text.Json em C#

A serialização e desserialização de objetos para JSON são tarefas comuns em projetos .NET. No ecossistema .NET, o “System.Text.Json” se destaca como uma biblioteca nativa, eficiente e flexível para lidar com JSON. Embora seja amplamente utilizado para casos simples, situações complexas frequentemente surgem, como trabalhar com formatos de data e hora personalizados ou lidar com enums representados como strings no JSON. Neste artigo, exploraremos como o “System.Text.Json” pode ser adaptado para atender a essas demandas, com foco na criação e uso de conversores personalizados. Introdução ao System.Text.Json Antes de nos aprofundarmos em técnicas mais avançadas, é importante compreender como funciona a serialização e desserialização básica com o System.Text.Json. Essa biblioteca oferece suporte nativo para converter objetos C# em JSON e vice-versa. Vejamos o exemplo abaixo: using System.Text.Json; public class Pessoa { public string Nome { get; set; } public int Idade { get; set; } } var pessoa = new Pessoa { Nome = “Joao”, Idade = 30 }; // Serialização: Objeto C# para JSON string json = JsonSerializer.Serialize(pessoa); Console.WriteLine(json); //Resultado apresentado no console: {“Nome”:”João”,”Idade”:30} // Desserialização: JSON para Objeto C# var desserializado = JsonSerializer.Deserialize(json); Console.WriteLine($”{desserializado.Nome}, {desserializado.Idade}”); //Resultado apresentado no console: Joao, 30 Neste exemplo, o “System.Text.Json” utiliza automaticamente os nomes das propriedades da classe para mapear os campos no JSON. Esse comportamento é conveniente em cenários onde o formato do JSON corresponde diretamente à estrutura da classe. Agora que entendemos o funcionamento básico, vamos explorar como lidar com situações mais complexas Conversores personalizados Conversores personalizados são úteis quando: O formato JSON não corresponde diretamente às propriedades da classe. É necessário manipular valores especiais, como enums representados por strings ou formatos de data específicos. O JSON contém propriedades dinâmicas ou ausentes. Criando um conversor personalizado Os conversores personalizados são implementados estendendo a classe “JsonConverter<T>”. Para usar um conversor, você o registra no nível da propriedade (com [JsonConverter]) ou globalmente, nas opções do serializador. Imagine um sistema que utiliza o formato de data “dd/MM/yyyy”. Um conversor personalizado pode mapear esse formato para “DateTime”. { “Cliente”: “Maria”, “DataEntrega “: “25/12/1990″ } Para realizarmos a serialização do json em um objeto C#, vamos criar uma classe chamada “Pedido”: public class Pedido { public string Cliente { get; set; } [JsonConverter(typeof(DateFormatConverter))] public DateTime DataEntrega { get; set; } } A classe “Pedido” possui as propriedades “Cliente” um campo string simples, que será mapeado automaticamente no JSON e “DataEntrega” um campo “DateTime”, que usa o atributo “[JsonConverter]” para associar um conversor personalizado, “DateFormatConverter” que será criado logo abaixo. Esse atributo é necessário porque o formato padrão de serialização para “DateTime” no “System.Text.Json” usa ISO 8601 (“yyyy-MM-ddTHH:mm:ss.fffZ”), o que não corresponde ao formato específico (“dd/MM/yyyy”) usado neste exemplo. Agora vamos criar o conversor personalizado: public class DateFormatConverter : JsonConverter { private const string DateFormat = “dd/MM/yyyy”; public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return DateTime.ParseExact(reader.GetString(), DateFormat, null); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString(DateFormat)); } } O “DateFormatConverter” é uma classe personalizada que estende o comportamento do “System.Text.Json” ao serializar e desserializar datas no formato “dd/MM/yyyy”. Ele herda de “JsonConverter<DateTime>” e implementa dois métodos principais: Read (Desserialização): Esse método é chamado quando um JSON está sendo convertido em um objeto C#. Ele utiliza o “Utf8JsonReader” para ler o valor da propriedade no JSON e, em seguida, usa “DateTime.ParseExact” para interpretar a string no formato específico (“dd/MM/yyyy”) e transformá-la em um objeto “DateTime”.  Write (Serialização): Durante a serialização, este método é chamado para converter um valor “DateTime” do objeto C# em uma string JSON formatada como “dd/MM/yyyy”. O método utiliza o “Utf8JsonWriter” e chama “value.ToString(DateFormat)” para garantir que o formato de saída corresponda ao esperado. Para testar podemos utilizar o seguinte código: var json = @”{ “”Cliente””: “”João””, “”DataEntrega””: “”15/12/2024″” }”; var pedido = JsonSerializer.Deserialize(json); Console.WriteLine($”{pedido.Cliente} – {pedido.DataEntrega:yyyy-MM-dd}”); Tratamento de Enums como Strings Enums podem ser representados como strings personalizadas no JSON, e um conversor facilita essa tradução, permitindo que os valores do enum sejam manipulados de maneira mais flexível ao serializar ou desserializar objetos: public enum Status { Ativo, Inativo } public class StatusConverter : JsonConverter { public override Status Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetString() switch { “ativo” => Status.Ativo, “inativo” => Status.Inativo, _ => throw new JsonException(“Valor inválido”) }; } public override void Write(Utf8JsonWriter writer, Status value, JsonSerializerOptions options) { writer.WriteStringValue(value == Status.Ativo ? “ativo” : “inativo”); } } No código acima, o método “Read” verifica qual valor no JSON corresponde aos casos definidos no “switch” e retorna o valor do enum correspondente. Já o método “Write” converte o valor do enum em uma string para ser escrita no JSON. Agora, vamos criar a classe “Usuario” que irá receber o enum “Status” com o conversor personalizado: public class Usuario { [JsonConverter(typeof(StatusConverter))] public Status Status { get; set; } } Com isso, podemos criar um exemplo para testar o processo de desserialização: var usuarioJson = @”{ “”Status””: “”ativo”” }”; var usuario = JsonSerializer.Deserialize(usuarioJson); Console.WriteLine(usuario.Status); Neste exemplo, o JSON com a string “ativo” é convertido para o valor “Status.Ativo” na propriedade “Status” do objeto “Usuario”. Esse processo permite um mapeamento flexível entre os valores do enum e suas representações em string no JSON, facilitando a integração com sistemas que utilizam formatos específicos para representar enums. Manipulação de Dados Dinâmicos Em muitos casos, ao trabalhar com dados JSON, a estrutura pode ser imprevisível ou variar de acordo com diferentes cenários, como quando consumimos APIs externas ou lidamos com sistemas de terceiros. Nesses casos, não é prático ou possível definir previamente classes para representar todos os formatos possíveis de JSON. Para esses cenários, o “System.Text.Json” fornece ferramentas como “JsonElement” e “JsonDocument”, que permitem acessar e manipular os dados de forma dinâmica, sem a necessidade de modelos específicos.  Para entendermos melhor, imagine que você está consumindo uma API que retorna informações de pedidos, mas o formato do JSON pode variar dependendo dos itens no pedido ou de atualizações no sistema. var json = @” { “”PedidoId””: 12345, “”Cliente””: “”João Silva””, “”Itens””: [ { “”Produto””: “”Notebook””,