Category Archives: DotNet

IIS – Dimensionando Aplicações

Prompt

Existe alguma técnica ou metodologia para dimensionar recursos de uma aplicação .Net, a ser implantada no servidor IIS compartilhado? Entendo que aplicações CRUD consomem bem menos recursos, do que aplicações de tenham Multi-thread ou processamento de carga de dados, que podem consumir mais memória e CPU, em momentos específicos.

Gemini

Sim, existem técnicas e metodologias para dimensionar recursos de uma aplicação .NET no IIS Compartilhado, mas o processo é mais uma arte combinada de monitoramento, otimização e experimentação do que uma fórmula exata. A principal diferença no ambiente compartilhado é que você tem menos controle sobre o servidor como um todo e precisa ser muito eficiente no uso do seu Application Pool.


 

🔍 Metodologia de Dimensionamento em IIS Compartilhado

 

O dimensionamento eficaz neste cenário se baseia em três pilares: Análise de Carga (Benchmarking), Otimização de Código e Configuração e Monitoramento Contínuo.

 

1. Análise de Carga (Benchmarking)

 

Você precisa simular o uso para descobrir os “picos” de consumo.

  • Identifique as Transações Chave (Critical Paths): Liste as funcionalidades que consomem mais CPU e memória, como as que você mencionou (Multi-thread, processamento de carga de dados) e também consultas complexas ao banco de dados.
  • Teste de Carga (Load Testing): Use ferramentas como Apache JMeter, Visual Studio Load Testing ou Gatling para simular um número esperado de usuários simultâneos (ou até um pouco acima) acessando sua aplicação.
    • Isso revelará o uso máximo de CPU e memória que sua aplicação atinge.
  • Estabeleça Limites (Baselines): Meça o consumo de recursos nessas transações sob carga. Por exemplo: “O endpoint /processa-dados consome X MB de RAM e Y% de CPU por 5 segundos com 50 usuários simultâneos.”

 

2. Otimização Específica para .NET e IIS

 

Como você não controla o hardware, precisa garantir que seu código e a configuração do IIS sejam o mais eficientes possível.

 

🚀 Otimização do Código .NET

 

  • Assíncrono (async/await): Para operações de E/S (I/O) intensivas (como chamadas a banco de dados, serviços externos ou acesso a arquivos), use o modelo async e await. Isso libera o Thread do pool de threads do IIS enquanto a operação externa está em espera, permitindo que o Worker Process atenda a outras requisições. Isso aumenta a escalabilidade sem aumentar o uso de threads.
  • Gerenciamento de Memória: Minimize a alocação de objetos para reduzir a frequência do Coletor de Lixo (Garbage Collector – GC), que é um processo intensivo em CPU e pode causar picos de latência.
  • Otimização de Consultas SQL: Consultas lentas são um gargalo comum. Otimize-as (índices, stored procedures) para reduzir o tempo de retenção da conexão e o tempo de CPU no lado do servidor de aplicação.

 

⚙️ Configurações do Application Pool (IIS)

 

Em um ambiente compartilhado, você geralmente gerencia apenas o seu Application Pool.

  • Reciclagem (Recycling): Configure a reciclagem do Application Pool para ocorrer em horários de baixo tráfego (ex: madrugada) ou com base em um limite de uso de memória (Private Memory Limit). Evite a reciclagem baseada em um número fixo de requisições, pois isso pode causar picos de reinicialização.
  • Tempo Limite de Ociosidade (Idle Time-out): Se sua aplicação for de baixo tráfego e você quiser que ela consuma zero recursos quando inativa, o padrão (geralmente 20 minutos) está ok. Mas se a aplicação precisar de resposta imediata, o Application Pool deve ser configurado para nunca desligar (Idle Time-out = 0) ou usar o Always Running se o provedor permitir.
  • Web Garden (Maximum Worker Processes): Esta é uma configuração avançada e arriscada em ambientes compartilhados. O Web Garden executa múltiplos processos de trabalho para um único Application Pool.
    • Prós: Pode melhorar a utilização da CPU em servidores multicore e aumentar a resiliência (se um Worker Process falhar, os outros continuam).
    • Contras: Aumenta drasticamente o consumo de memória (cada processo carrega toda a aplicação) e requer que sua aplicação seja “thread-safe” e não armazene estado em memória (use Redis ou outro cache distribuído). Use com cautela e sob teste.

 

3. Monitoramento Contínuo 📊

 

A fase mais crítica, especialmente para identificar a necessidade de redimensionamento (o que você chamou de “momentos específicos” de alto consumo).

  • Contadores de Performance: Monitore os principais contadores do Windows Server/IIS:
    • Memória (Private Bytes): O quanto seu processo (w3wp.exe) realmente consome.
    • CPU (% Processor Time): Uso de CPU do processo.
    • Requisições Ativas e na Fila (Requests / Sec, Current Queue Length): Indicam se o IIS está conseguindo processar a carga a tempo.
    • Tempo de Resposta (Total Request Execution Time): Latência das suas requisições.
  • Ferramentas de APM (Application Performance Monitoring): Ferramentas como Application Insights ou Dynatrace são essenciais para mapear o uso de recursos diretamente para o código, ajudando a identificar a linha ou função exata que está causando o pico.

Resumo: Para aplicações CRUD simples, o padrão do IIS geralmente é suficiente. Para aplicações com processamento intensivo (Multi-thread, carga de dados), você deve otimizar o código para ser async, minimizar alocação de memória e monitorar para ajustar os limites de reciclagem do Application Pool e, em último caso, solicitar mais recursos do host (ou considerar um ambiente dedicado/cloud se o shared não suportar).

Gostaria de ajuda para encontrar uma ferramenta de load testing gratuita para testar sua aplicação?

Prompt

O IIS tem ferramenta para análise de Tempo de Resposta (latência entre requisições) sem necessidade de implementação no código?

Gemini

Sim, o IIS possui ferramentas nativas para análise de tempo de resposta (latência entre requisições) sem a necessidade de implementar código na sua aplicação .NET.


 

🛠️ Ferramentas Nativas do IIS para Análise de Latência

 

As duas principais ferramentas que o IIS oferece para este fim são:

 

1. Logs do IIS (IIS Logs) 📝

 

O IIS registra automaticamente detalhes de cada requisição em arquivos de log. Você pode configurar quais campos são registrados, mas os essenciais para a latência estão ativados por padrão:

  • time-taken (Tempo Levado): Este é o campo mais importante. Ele registra, em milissegundos, o tempo total que o IIS levou para processar a requisição e enviar a resposta ao cliente.
    • Como usar: Basta analisar os arquivos de log (geralmente localizados em C:\inetpub\logs\LogFiles) utilizando ferramentas de processamento de log ou scripts simples. Ao ordenar os logs por time-taken, você identifica imediatamente as requisições mais lentas.

 

2. Ferramenta de Solução de Problemas do IIS (IIS Troubleshooting) ⚙️

 

O próprio Gerenciador do IIS (IIS Manager) e o Windows Server oferecem recursos para rastreamento:

  • Rastreamento de Solicitações Falhas (Failed Request Tracing – FREB): Embora o nome sugira apenas falhas, o FREB é uma ferramenta poderosa de diagnóstico que você pode configurar para rastrear todas as requisições ou apenas aquelas que excedem um determinado limite de tempo (por exemplo, mais de 1 segundo).
    • Como usar: Você configura uma regra no IIS Manager. Quando ativado, o FREB gera um arquivo XML detalhado que mostra exatamente quanto tempo a requisição passou em cada estágio do pipeline do IIS (Autenticação, Autorização, Módulos, Manipulador da Aplicação, etc.). Isso é crucial para determinar se a lentidão está no seu código ou em alguma fase do processamento do IIS.
    • Vantagem: Oferece uma visão muito mais granular do que o time-taken do log simples.

 

📊 Contadores de Desempenho (Performance Counters)

 

Embora não sejam uma “ferramenta de log” por si só, os contadores de desempenho do Windows Server, acessíveis via Monitor de Desempenho (PerfMon), são cruciais para a análise em tempo real e não exigem código na aplicação.

Você pode monitorar métricas diretamente relacionadas à latência do IIS e dos Application Pools:

  • Web Service:
    • Total Method Requests/sec: Taxa de requisições por segundo.
  • ASP.NET Applications (ou ASP.NET Core):
    • Requests Executing: Quantidade de requisições em processamento.
    • Request Wait Time: Tempo (em ms) que as requisições estão esperando na fila antes de serem processadas. Um valor alto aqui indica que a CPU ou os Worker Processes estão sobrecarregados.

Ao usar esses contadores, você pode correlacionar picos de latência que aparecem nos logs com picos de CPU ou alto tempo de espera na fila, diagnosticando rapidamente gargalos sem modificar o código da aplicação.

API CSV DotNet

Prompt

Write code for – Pretendo criar uma API RESTful, usando .Net.
Qual versão, frameworks, design patterns e estrutura do projeto mais recomendados?
API básica:
1. POST – Recebe um arquivo CSV, onde cada header representa uma coluna de uma tabela específica, por exemplo “order.csv” será relacionada à tabela “order”. Banco de dados é Oracle.
2. A leitura da planilha deve ser otimizada, usando conceito de multi-thread
3. O processo de INSERT também deve ser otimizado, por exemplo utilizar conceito “insert batch”
4. Caso ocorra alguma linha, identificar a linha e armazenar num JSON que será apresentado no Body do Response

Abacus AI – Claude Sonnet 4.5

Recomendações para API RESTful em .NET

Versão e Frameworks Recomendados

  • .NET 8 (LTS – suporte até nov/2026)
  • ASP.NET Core Web API
  • Oracle.ManagedDataAccess.Core (driver Oracle oficial)
  • CsvHelper (leitura otimizada de CSV)

Design Patterns e Arquitetura

  • Clean Architecture / Vertical Slice Architecture
  • Repository Pattern (acesso a dados)
  • Unit of Work (transações)
  • CQRS (separar commands/queries)
  • Result Pattern (tratamento de erros)

Estrutura do Projeto

CsvImportApi/
├── src/
│   ├── CsvImportApi.Api/              # Controllers, Middleware
│   ├── CsvImportApi.Application/      # Services, DTOs, Interfaces
│   ├── CsvImportApi.Domain/           # Entities, Value Objects
│   └── CsvImportApi.Infrastructure/   # Repositories, Oracle Context
└── tests/

Código Completo da API

1. CsvImportApi.Api/Controllers/CsvImportController.cs

using CsvImportApi.Application.DTOs;
using CsvImportApi.Application.Services;
using Microsoft.AspNetCore.Mvc;

namespace CsvImportApi.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class CsvImportController : ControllerBase
{
    private readonly ICsvImportService _csvImportService;
    private readonly ILogger<CsvImportController> _logger;

    public CsvImportController(ICsvImportService csvImportService, ILogger<CsvImportController> logger)
    {
        _csvImportService = csvImportService;
        _logger = logger;
    }

    [HttpPost("upload")]
    [RequestSizeLimit(100_000_000)] // 100MB
    [RequestFormLimits(MultipartBodyLengthLimit = 100_000_000)]
    public async Task<IActionResult> UploadCsv(IFormFile file, CancellationToken cancellationToken)
    {
        if (file == null || file.Length == 0)
            return BadRequest(new { error = "Arquivo não fornecido ou vazio" });

        if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            return BadRequest(new { error = "Apenas arquivos CSV são permitidos" });

        try
        {
            var tableName = Path.GetFileNameWithoutExtension(file.FileName);
            
            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportCsvAsync(stream, tableName, cancellationToken);

            return Ok(result);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao processar arquivo CSV");
            return StatusCode(500, new { error = "Erro interno ao processar arquivo", details = ex.Message });
        }
    }
}

2. CsvImportApi.Application/DTOs/ImportResultDto.cs

namespace CsvImportApi.Application.DTOs;

public class ImportResultDto
{
    public string TableName { get; set; } = string.Empty;
    public int TotalRows { get; set; }
    public int SuccessfulRows { get; set; }
    public int FailedRows { get; set; }
    public TimeSpan ProcessingTime { get; set; }
    public List<ErrorDetail> Errors { get; set; } = new();
}

public class ErrorDetail
{
    public int LineNumber { get; set; }
    public string ErrorMessage { get; set; } = string.Empty;
    public Dictionary<string, string> RowData { get; set; } = new();
}

3. CsvImportApi.Application/Services/ICsvImportService.cs

using CsvImportApi.Application.DTOs;

namespace CsvImportApi.Application.Services;

public interface ICsvImportService
{
    Task<ImportResultDto> ImportCsvAsync(Stream csvStream, string tableName, CancellationToken cancellationToken);
}

4. CsvImportApi.Application/Services/CsvImportService.cs

using System.Collections.Concurrent;
using System.Diagnostics;
using CsvHelper;
using CsvHelper.Configuration;
using CsvImportApi.Application.DTOs;
using CsvImportApi.Infrastructure.Repositories;
using System.Globalization;

namespace CsvImportApi.Application.Services;

public class CsvImportService : ICsvImportService
{
    private readonly IBulkInsertRepository _bulkInsertRepository;
    private readonly ILogger<CsvImportService> _logger;
    private const int BATCH_SIZE = 1000;
    private const int MAX_DEGREE_OF_PARALLELISM = 4;

    public CsvImportService(IBulkInsertRepository bulkInsertRepository, ILogger<CsvImportService> logger)
    {
        _bulkInsertRepository = bulkInsertRepository;
        _logger = logger;
    }

    public async Task<ImportResultDto> ImportCsvAsync(Stream csvStream, string tableName, CancellationToken cancellationToken)
    {
        var stopwatch = Stopwatch.StartNew();
        var result = new ImportResultDto { TableName = tableName };
        var errors = new ConcurrentBag<ErrorDetail>();

        try
        {
            var config = new CsvConfiguration(CultureInfo.InvariantCulture)
            {
                HasHeaderRecord = true,
                MissingFieldFound = null,
                BadDataFound = null
            };

            using var reader = new StreamReader(csvStream);
            using var csv = new CsvReader(reader, config);

            // Lê o header
            await csv.ReadAsync();
            csv.ReadHeader();
            var headers = csv.HeaderRecord ?? throw new InvalidOperationException("CSV sem header");

            // Lê todos os registros em batches
            var allRecords = new List<Dictionary<string, string>>();
            int lineNumber = 2; // Linha 1 é o header

            while (await csv.ReadAsync())
            {
                try
                {
                    var record = new Dictionary<string, string>();
                    foreach (var header in headers)
                    {
                        record[header] = csv.GetField(header) ?? string.Empty;
                    }
                    record["_LineNumber"] = lineNumber.ToString();
                    allRecords.Add(record);
                }
                catch (Exception ex)
                {
                    errors.Add(new ErrorDetail
                    {
                        LineNumber = lineNumber,
                        ErrorMessage = $"Erro ao ler linha: {ex.Message}",
                        RowData = new Dictionary<string, string>()
                    });
                }
                lineNumber++;
            }

            result.TotalRows = allRecords.Count;

            // Processa em batches usando multi-threading
            var batches = allRecords
                .Select((record, index) => new { record, index })
                .GroupBy(x => x.index / BATCH_SIZE)
                .Select(g => g.Select(x => x.record).ToList())
                .ToList();

            var parallelOptions = new ParallelOptions
            {
                MaxDegreeOfParallelism = MAX_DEGREE_OF_PARALLELISM,
                CancellationToken = cancellationToken
            };

            await Parallel.ForEachAsync(batches, parallelOptions, async (batch, ct) =>
            {
                try
                {
                    await _bulkInsertRepository.BulkInsertAsync(tableName, batch, ct);
                    Interlocked.Add(ref result.SuccessfulRows, batch.Count);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Erro ao inserir batch");
                    
                    // Tenta inserir linha por linha para identificar erros específicos
                    foreach (var record in batch)
                    {
                        try
                        {
                            await _bulkInsertRepository.BulkInsertAsync(tableName, new List<Dictionary<string, string>> { record }, ct);
                            Interlocked.Increment(ref result.SuccessfulRows);
                        }
                        catch (Exception innerEx)
                        {
                            var lineNum = int.Parse(record["_LineNumber"]);
                            var rowData = new Dictionary<string, string>(record);
                            rowData.Remove("_LineNumber");

                            errors.Add(new ErrorDetail
                            {
                                LineNumber = lineNum,
                                ErrorMessage = innerEx.Message,
                                RowData = rowData
                            });
                            Interlocked.Increment(ref result.FailedRows);
                        }
                    }
                }
            });

            result.Errors = errors.OrderBy(e => e.LineNumber).ToList();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro crítico no processamento do CSV");
            throw;
        }
        finally
        {
            stopwatch.Stop();
            result.ProcessingTime = stopwatch.Elapsed;
        }

        return result;
    }
}

5. CsvImportApi.Infrastructure/Repositories/IBulkInsertRepository.cs

namespace CsvImportApi.Infrastructure.Repositories;

public interface IBulkInsertRepository
{
    Task BulkInsertAsync(string tableName, List<Dictionary<string, string>> records, CancellationToken cancellationToken);
}

6. CsvImportApi.Infrastructure/Repositories/OracleBulkInsertRepository.cs

using Oracle.ManagedDataAccess.Client;
using System.Data;
using System.Text;

namespace CsvImportApi.Infrastructure.Repositories;

public class OracleBulkInsertRepository : IBulkInsertRepository
{
    private readonly string _connectionString;
    private readonly ILogger<OracleBulkInsertRepository> _logger;

    public OracleBulkInsertRepository(IConfiguration configuration, ILogger<OracleBulkInsertRepository> logger)
    {
        _connectionString = configuration.GetConnectionString("OracleDb") 
            ?? throw new InvalidOperationException("Connection string não configurada");
        _logger = logger;
    }

    public async Task BulkInsertAsync(string tableName, List<Dictionary<string, string>> records, CancellationToken cancellationToken)
    {
        if (!records.Any()) return;

        using var connection = new OracleConnection(_connectionString);
        await connection.OpenAsync(cancellationToken);

        using var transaction = connection.BeginTransaction();
        try
        {
            // Remove o campo auxiliar _LineNumber
            var columns = records.First().Keys.Where(k => k != "_LineNumber").ToList();
            
            // Monta o comando INSERT com array binding (bulk insert otimizado)
            var sql = BuildBulkInsertCommand(tableName, columns, records.Count);

            using var command = new OracleCommand(sql, connection);
            command.Transaction = transaction;
            command.ArrayBindCount = records.Count;

            // Configura os parâmetros com array binding
            foreach (var column in columns)
            {
                var parameter = new OracleParameter
                {
                    ParameterName = $":{column}",
                    OracleDbType = OracleDbType.Varchar2,
                    Size = 4000
                };

                var values = records.Select(r => r.ContainsKey(column) ? r[column] : (object)DBNull.Value).ToArray();
                parameter.Value = values;
                
                command.Parameters.Add(parameter);
            }

            await command.ExecuteNonQueryAsync(cancellationToken);
            await transaction.CommitAsync(cancellationToken);

            _logger.LogInformation($"Inseridos {records.Count} registros na tabela {tableName}");
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync(cancellationToken);
            _logger.LogError(ex, $"Erro ao inserir batch na tabela {tableName}");
            throw;
        }
    }

    private string BuildBulkInsertCommand(string tableName, List<string> columns, int recordCount)
    {
        var columnList = string.Join(", ", columns);
        var parameterList = string.Join(", ", columns.Select(c => $":{c}"));
        
        return $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList})";
    }
}

7. CsvImportApi.Api/Program.cs

using CsvImportApi.Application.Services;
using CsvImportApi.Infrastructure.Repositories;

var builder = WebApplication.CreateBuilder(args);

// Configuração de serviços
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Dependency Injection
builder.Services.AddScoped<ICsvImportService, CsvImportService>();
builder.Services.AddScoped<IBulkInsertRepository, OracleBulkInsertRepository>();

// Configuração de upload de arquivos grandes
builder.Services.Configure<Microsoft.AspNetCore.Http.Features.FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = 100_000_000; // 100MB
});

var app = builder.Build();

// Pipeline HTTP
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

8. appsettings.json

{
  "ConnectionStrings": {
    "OracleDb": "User Id=seu_usuario;Password=sua_senha;Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

9. CsvImportApi.Api.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="CsvHelper" Version="30.0.1" />
    <PackageReference Include="Oracle.ManagedDataAccess.Core" Version="23.4.0" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
  </ItemGroup>

</Project>

Recursos Implementados

Multi-threading: Parallel.ForEachAsync com controle de paralelismo
Batch Insert: Oracle Array Binding (até 1000 registros por batch)
Tratamento de erros: Captura linha específica e retorna no JSON
Performance: CsvHelper + StreamReader otimizado
Clean Architecture: Separação de responsabilidades
Logging: ILogger integrado
Cancellation Token: Suporte a cancelamento de operações

Exemplo de Response

{
  "tableName": "order",
  "totalRows": 5000,
  "successfulRows": 4998,
  "failedRows": 2,
  "processingTime": "00:00:03.4521234",
  "errors": [
    {
      "lineNumber": 150,
      "errorMessage": "ORA-01400: cannot insert NULL into (\"SCHEMA\".\"ORDER\".\"ID\")",
      "rowData": {
        "id": "",
        "customer": "John Doe",
        "amount": "100.50"
      }
    }
  ]
}