Criando API com .NET

rodbarbosa

Rod Barbosa

Posted on November 6, 2022

Criando API com .NET

Para aqueles que programam a mais de 15 anos com certeza já em algum momento colocou a mão em um monólito, seja este web ou desktop. Mas com a evolução tecnológica vemos já uma clara divisão de responsabilidade e de entrega de dados por meio de APIs.

Neste post estarei abordando a construção de uma API REST utilizando .NET. Lembro que em .NET ao criar um projeto 'WebApi' o mesmo irá utilizar um template que seguido terá um resultará em uma API funcional.
O projeto criado pode ser encontrado no endereço https://github.com/rodabarbosa/apitemplate.

Para um entendimento, normalmente não digo que o código é ruim. Para poder dizer isto devo conhecer e ter experiência trabalhando com a pessoa que escreveu o código. Prefiro dizer que a pessoa fez o melhor com o conhecimento que tinha no momento.

Pensamento e explicações a parte vamos para a criação da API.

Requerimentos

Então para criação de nossa API será necessário ter instalado no computador:

  • DOTNET 6
  • Editor ou IDE de sua preferência. Ex.: VSCode, Visual Studio 2022, Rider, etc.

Objetivo

Criar uma API para entrega da dados do tempo (Weather Forecast), esta é a mesma base que é entregue junto ao template do 'webapi' do dotnet.

Criar tratamento global de erro onde o objeto de retorno é comum em todos os erros que possam ocorrer dentro da aplicação. Isto irá facilitar um tratamento para consumidor da API.

API deve conter Swagger para documentação. Este item já irá estar configurado e ativo já com origem no template entregue pelo .NET.

Criar uma estrutura para atender a responsabilidade única de cada recurso e facilitar a manutenção quando necessário;

Criando o projeto

Abra o terminal e vá até o diretório onde será armazenado o projeto.

Uma vez no diretório desejado digite o seguinte comando:

dotnet new sln -n ApiTemplate
Enter fullscreen mode Exit fullscreen mode

Estou criando um arquivo para minha solução e a minha solução irá se chamar ApiTemplate.

Então, faça o comando para criar o projeto de webapi:

dotnet new webapi -n ApiTemplate.WebApi
Enter fullscreen mode Exit fullscreen mode

Em seguida vamos criar alguns projeto do tipo biblioteca para armazenar regras e processos.

dotnet new classlib -n ApiTemplate.Shared
dotnet new classlib -n ApiTemplate.Domain
dotnet new classlib -n ApiTemplate.Infra.Data
Enter fullscreen mode Exit fullscreen mode

Para informar que todos estes projetos contemplam a minha solução utilizo o seguinte comando:

dotnet sln ApiTemplate.sln add (ls -r **/*.csproj)
Enter fullscreen mode Exit fullscreen mode

No meu caso estou utilizando o powershell do windows, se você estiver utilizando um sistema operacional diferente ou mesmo um terminal diferente recomendo a verificação da documentação no site da Microsoft.

Alternativas

Caso você esteja utilizando o Visual Studio 2022 basta ir em novo projeto, selecionar o tipo WebApi, informar o diretório onde deseja salvar e clicar em criar.

Depois em Solution Explore clique com o botão direito e vá em Add > New Project.

Ou seja, caso esteja utilizando uma IDE o processo de criação da solução e projeto é bem simples.

Estrutura do projeto

Como visto na parte anterior criamos alguns projetos do tipo biblioteca para separarmos nosso código. Então temos um projeto com o nome de Domain, apesar do nome este não será domínio como especificado/definido na arquitetura DDD, e sim, apenas uma separação logica de nossas entidades.

Temos Shared que irá armazenar código comum e de utilização em todos os níveis da solução. Este projeto não deve conter regras, mas isto não uma definição escrita em pedra sempre analise a necessidade e utilize o bom senso.

Também temos Infra.Data que é onde iremos armazenar as instruções para banco de dados.

E por fim, WebApi que é o nosso projeto de interface de dados.

Projeto Domain

Neste projeto foi criado um namespace, um diretório dentro do projeto, chamado Entities. Dentro deste namespace criei a classe WeatherForecast. Explicando melhor, como disse anteriormente o projeto WebApi já vai vir com uma base que é sobre WeatherForecast, então precisei mover a classe existente do projeto WebApi para meu namespace e ajustar a definição do mesmo dentro do arquivo.

Dentro do projeto também criei um namespace chamado Repository onde coloquei classes do tipo interface com definições de métodos que irão existir com instruções de banco de dado. Resumidamente, na interface não existe implementação de métodos, e sim, apenas informações que aqueles métodos existirão em uma classe que ainda não sei qual será.

public interface IWeatherForecastRepository
{
    IQueryable<WeatherForecast> Get(Expression<Func<WeatherForecast, bool>> predicate = null!);
    WeatherForecast? Get(Guid id);
    Task<WeatherForecast?> GetAsync(Guid id);
    void Add(WeatherForecast weatherForecast);
    Task AddAsync(WeatherForecast weatherForecast);
    void Update(WeatherForecast weatherForecast);
    Task UpdateAsync(WeatherForecast weatherForecast);
    void Delete(Guid id);
    void Delete(WeatherForecast weatherForecast);
    Task DeleteAsync(Guid id);
    Task DeleteAsync(WeatherForecast? weatherForecast);
}
Enter fullscreen mode Exit fullscreen mode

Projeto Shared

Neste projeto estão os códigos que são compartilhados entre os demais projetos e estes códigos estão livre de regras do meu negócio. Com isto em mente, neste projeto foi criado extensões, exceções e modelos de uso comum.

Exceções

Para as exceções foi criada cada uma para servir a um determinado proposito. Irei apresentar uma e as demais seguem esta mesma estrutura.

public class BadRequestException : Exception
{
    private const string DefaultMessage = "Bad Request";

    public BadRequestException(string? message = DefaultMessage) : this(message, default) { }

    public BadRequestException(Exception? innerException) : this(DefaultMessage, innerException) { }

    public BadRequestException(string? message, Exception? innerException) : base(DefineMessage(message, DefaultMessage), innerException) { }

    protected BadRequestException(SerializationInfo info, StreamingContext context) : base(info, context) { }

    private static string? DefineMessage(string? message, string? fallbackMessage)
    {
        return string.IsNullOrEmpty(message?.Trim()) ? fallbackMessage : message;
    }

    public static void ThrowIf(bool condition, string? message = DefaultMessage, Exception? innerException = null)
    {
        if (condition)
            throw new BadRequestException(message, innerException);
    }
}
Enter fullscreen mode Exit fullscreen mode

Em todas as classes de exceções criado sempre adiciono um método estatico com o nome de ThrowIf que utilizo para facilitar a leitura do código. O funcionado deste método é basicamente que quando a condição for verdadeira é disparado a exceção com a mensagem informada.

Com esta estrutura também foram criadas as exceções:

  • DbRegisterExistsException: Utilizando na operação de criar um registro na banco de dados com dados já existentes;
  • DeleteFailureException: Como o próprio nome já sugere, utilizando quando ocorre um erro na operação de excluir um registro;
  • NotFoundException: Este tipo é utilizando nos métodos de busca que não é encontrado nenhum resultado. Em atualização e exclusão quando o registro informado não é encontrado.
  • SaveFailureException: Utilizado para quando ocorrer um erro na operação de salvar um registro em banco de dados;
  • ValidationException: Utilizado quando existir erro na validação dos dados recebidos em algum método;

Para a classe ValidationException existe uma variação na estrutura da classe, pois irei registrar uma lista de chave e valor referente ao(s) erro(s):

public class ValidationException : Exception
{
    private const string DefaultMessage = "One or more validation failure have occurred.";

    private ValidationException(string message = DefaultMessage) : this(message, null) { }

    public ValidationException(Exception? innerException) : this(DefaultMessage, innerException) { }

    private ValidationException(string? message, Exception? innerException) : base(DefineMessage(message, DefaultMessage), innerException) { }

    public ValidationException(IEnumerable<ValidationFailure> failures) : this()
    {
        IEnumerable<ValidationFailure> validationFailures = failures.ToList();
        var propertyNames = validationFailures
            .Select(e => e.PropertyName)
            .Distinct();

        foreach (var propertyName in propertyNames)
        {
            var propertyFailures = validationFailures
                .Where(e => e.PropertyName == propertyName)
                .Select(e => e.ErrorMessage)
                .ToArray();

            Failures.Add(propertyName, propertyFailures);
        }
    }

    protected ValidationException(SerializationInfo info, StreamingContext context) : base(info, context) { }

    public IDictionary<string, string[]> Failures { get; } = new Dictionary<string, string[]>();

    private static string? DefineMessage(string? message, string? fallbackMessage)
    {
        return string.IsNullOrEmpty(message?.Trim()) ? fallbackMessage : message;
    }

    public static void ThrowIf(bool condition, IEnumerable<ValidationFailure> failures)
    {
        if (condition)
            throw new ValidationException(failures);
    }

    public static void When(bool condition, string? message = DefaultMessage, Exception? innerException = null)
    {
        if (condition)
            throw new ValidationException(message, innerException);
    }
}
Enter fullscreen mode Exit fullscreen mode

Extensões

Para extensões foram criados métodos para serialização e deserilização de JSOM, conversão de graus célsius para fahrenheit e vice-versa.

Para a manipulação do JSON a ideia é centralizar os métodos e caso ocorra a necessidade de alterar a biblioteca do mesmo, basta alterar a extensão. Isto facilita futuras manutenções.

public static class JsonExtension
{
    private static readonly JsonSerializerOptions _jsonSerializerOptionsDeserialization = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true,
        WriteIndented = false,
        Encoder = JavaScriptEncoder.Default,
        IgnoreReadOnlyFields = true,
        IgnoreReadOnlyProperties = true
    };

    private static readonly JsonSerializerOptions _jsonSerializerOptionsSerialization = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true,
        WriteIndented = false,
        Encoder = JavaScriptEncoder.Default,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    public static string ToJson(this object value)
    {
        return JsonSerializer.Serialize(value, _jsonSerializerOptionsSerialization);
    }

Enter fullscreen mode Exit fullscreen mode

Para conversão de graus, retirei o código de conversão de célsius para fahrenheit existente do modelo gerado no template do WebApi e foi incluso o método para conversão de fahrenheit para célsius. Estes métodos serão utilizados tanto na entidade quanto no contrato.

public static class TemperatureExtension
{
    public static decimal ToFahrenheit(this decimal celsius)
    {
        return celsius * 9 / 5 + 32;
    }

    public static decimal ToCelsius(this decimal fahrenheit)
    {
        return (fahrenheit - 32) * 5 / 9;
    }
}
Enter fullscreen mode Exit fullscreen mode

Nota:

A entidade é utilizada junto ao banco de dados e o contrato é utilizando junto as requisições da API. O contrato não necessita ser um reflexo de uma entidade.

Modelos

O único modelo foi o utilizado para retorno da API caso ocorra algum problema na execução.

public sealed class ErrorModel
{
    [JsonPropertyName("code")] public int Code { get; init; }
    [JsonPropertyName("error")] public object? Error { get; init; }
    [JsonPropertyName("exception")] public string? Exception { get; init; }

#if !DEBUG
    [JsonIgnore]
#endif
    [JsonPropertyName("stacktrace")] public string? StackTrace { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

Projeto Infra.Data

Este projeto é responsável com a comunicação com o banco de dados. Neste projeto estou utilizando o EntityFramework para fazer esta gestão mas pode ser utilizado qualquer outro componente para esta função. Para esta amostra irei apenas armazenar os dados em memoria.

Para instalar o EntityFramework no diretório do projeto Infra.Data utilize o seguinte comando:

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.InMemory
Enter fullscreen mode Exit fullscreen mode

Deve ser consultado a provedor do banco de dados para obter qual a biblioteca necessária para se trabalhar com o banco escolhido.

Configurado as entidades com o banco

Esta parte é importante para especificar como uma entidade se comunica com uma determinada tabela do banco de dados. Para isto utilizo o FluentApi para escrever estas definições.

public class WeatherForecastConfiguration : IEntityTypeConfiguration<WeatherForecast>
{
    public void Configure(EntityTypeBuilder<WeatherForecast> builder)
    {
        builder.HasKey(x => x.Id);

        builder.Property(x => x.Id)
            .ValueGeneratedOnAdd();

        builder.Property(x => x.Date)
            .IsRequired();

        builder.Property(x => x.TemperatureCelsius)
            .IsRequired();

        builder.Property(x => x.Summary)
            .IsRequired(false);
    }
}
Enter fullscreen mode Exit fullscreen mode

Contexto do banco de dados

Como mencionado, esta sendo utilizado uma banco de dados em memoria para elaboração do código apresentado. Abaixo esta o contexto, o contexto é responsável por fazer a ponte entre o código e o banco de dados. Então com a definição feita utilizando o FluentApi o contexto irá conseguir transpor os dados armazenados em tabela do banco relacional para as classes entidades da aplicação.

public sealed class ApiTemplateContext : DbContext
{
    public ApiTemplateContext(DbContextOptions<ApiTemplateContext> options) : base(options)
    {
        Database.EnsureCreated();
    }

    public DbSet<WeatherForecast>? WeatherForecasts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApiTemplateContext).Assembly);
    }
}
Enter fullscreen mode Exit fullscreen mode

Implementando os repositórios

A implementação do repositório iremos escrever as instrução das operações que serão executadas no banco de dados. Lembrando que junto ao namespace Domain já criamos as interfaces com as assinaturas dos métodos.

Como o repositório se comunica com o banco de dados, então, iremos injetar o contexto na classe. Para isto colocamos que o construtor da classe deve ser informado uma instancia do contexto.

public sealed class WeatherForecastRepository : IWeatherForecastRepository
{
    private readonly ApiTemplateContext _context;

    public WeatherForecastRepository(ApiTemplateContext context)
    {
        _context = context;
    }

    public IQueryable<WeatherForecast> Get(Expression<Func<WeatherForecast, bool>>? predicate = default)
    {
        return predicate is null
            ? _context.WeatherForecasts!
            : _context.WeatherForecasts!.Where(predicate);
    }

    public WeatherForecast? Get(Guid id)
    {
        return _context.WeatherForecasts!.SingleOrDefault(x => x.Id == id);
    }

    public Task<WeatherForecast?> GetAsync(Guid id)
    {
        return _context.WeatherForecasts!.SingleOrDefaultAsync(x => x.Id == id);
    }

    public void Add(WeatherForecast weatherForecast)
    {
        AddCheckup(weatherForecast);
        _context.SaveChanges();
    }

    public Task AddAsync(WeatherForecast weatherForecast)
    {
        AddCheckup(weatherForecast);
        return _context.SaveChangesAsync();
    }

    public void Update(WeatherForecast weatherForecast)
    {
        _context.WeatherForecasts!.Update(weatherForecast);
        _context.SaveChanges();
    }

    public Task UpdateAsync(WeatherForecast weatherForecast)
    {
        _context.WeatherForecasts!.Update(weatherForecast);
        return _context.SaveChangesAsync();
    }

    public void Delete(Guid id)
    {
        var weather = Get(id);

        DeleteFailureException.ThrowIf(weather is null, $"Weather forecast {id} not found");

        Delete(weather);
    }

    public void Delete(WeatherForecast? weatherForecast)
    {
        DeleteFailureException.ThrowIf(weatherForecast is null, "Weather forecast was not informed.");

        _context.WeatherForecasts!.Remove(weatherForecast!);
        _context.SaveChanges();
    }

    public Task DeleteAsync(Guid id)
    {
        var weather = Get(id);

        DeleteFailureException.ThrowIf(weather is null, $"Weather forecast {id} not found");

        return DeleteAsync(weather);
    }

    public Task DeleteAsync(WeatherForecast? weatherForecast)
    {
        DeleteFailureException.ThrowIf(weatherForecast is null, "Weather forecast was not informed.");

        _context.WeatherForecasts!.Remove(weatherForecast!);
        return _context.SaveChangesAsync();
    }

    private void AddCheckup(WeatherForecast weatherForecast)
    {
        if (weatherForecast.Id == Guid.Empty)
            weatherForecast.Id = Guid.NewGuid();

        _context.WeatherForecasts!.Add(weatherForecast);
    }
}
Enter fullscreen mode Exit fullscreen mode

Projeto Application

Este projeto irá conter as regras necessárias para o funcionamento de nossa aplicação (sistema). Aqui iremos criar os contratos.

Contratos:

São as classes representando o conjunto de dados recebidos recebidos ou enviado ao usuário.

Contratos

Dentro do projeto foi criado um namespace com o nome de Contracts e nele armazenado as classes referente ao mesmo.

public class AuthenticateResponseContract
{
    public string Created { get; set; } = string.Empty;
    public string Expires { get; set; } = string.Empty;
    public string AccessToken { get; set; } = string.Empty;
    public string Username { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode
public class CreateWeatherForecastRequestContract
{
    public DateTime? Date { get; set; }
    public decimal? TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
public class GetWeatherForecastResponseContract
{
    public Guid Id { get; set; }
    public DateTime Date { get; set; }
    public decimal TemperatureCelsius { get; set; }
    public decimal TemperatureFahrenheit { get => TemperatureCelsius.ToFahrenheit(); }
    public string? Summary { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
public class UpdateWeatherForecastRequestContract
{
    public Guid? Id { get; set; }
    public DateTime? Date { get; set; }
    public decimal? TemperatureCelsius { get; set; }
    public decimal? TemperatureFahrenheit { get; set; }
    public string? Summary { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Validadores

Para os méotodos de autenticação, criar e atualiza novos métodos foi criado validadores. Na validação foi utilizado a biblioteca FluentValidation.

Para instalar not terminal, navege até o diretório do projeto Application e digite o comando:

dotnet add package FluentValidation
Enter fullscreen mode Exit fullscreen mode

Após a instalação da biblioteca pode ser escrito o validador.

public class CreateWeatherForecastValidator : AbstractValidator<CreateWeatherForecastRequestContract>
{
    /// <summary>
    /// Initializes a new instance of the <see cref="CreateWeatherForecastValidator"/> class.
    /// </summary>
    public CreateWeatherForecastValidator()
    {
        RuleFor(x => x.Date)
            .NotNull()
            .LessThanOrEqualTo(DateTime.Now.AddMinutes(1));

        RuleFor(x => x.TemperatureCelsius)
            .NotNull()
            .LessThanOrEqualTo(200)
            .GreaterThanOrEqualTo(-200);

        RuleFor(x => x.Summary)
            .Length(0, 250);
    }
}
Enter fullscreen mode Exit fullscreen mode
public class UpdateWeatherForecastValidator : AbstractValidator<UpdateWeatherForecastRequestContract>
{
    public UpdateWeatherForecastValidator()
    {
        RuleFor(x => x.Id)
            .NotNull()
            .NotEqual(Guid.Empty);

        RuleFor(x => x.Date)
            .NotNull()
            .LessThanOrEqualTo(DateTime.Now);

        RuleFor(x => x.TemperatureCelsius)
            .NotNull()
            .LessThanOrEqualTo(200)
            .GreaterThanOrEqualTo(-200);

        RuleFor(x => x.TemperatureFahrenheit)
            .NotNull();

        RuleFor(x => x)
            .Custom((contract, context) =>
            {
                if (contract.TemperatureCelsius is null)
                    return;

                if (contract.TemperatureFahrenheit is null)
                    return;

                var fahrenheit = contract.TemperatureCelsius?.ToFahrenheit();
                if (fahrenheit != contract.TemperatureFahrenheit)
                    context.AddFailure(nameof(contract.TemperatureFahrenheit), "Temperature Fahrenheit and Celsius does not match.");
            });

        RuleFor(x => x.Summary)
            .Length(0, 250);
    }
}
Enter fullscreen mode Exit fullscreen mode

Serviço

As classes criadas em serviço tem como objetivo executar uma determinada operação. Bom, sei que parece repetitivo mas pense que esta é a classe chave. Para exemplificar, a nossa aplicação terá uma chamada que será buscar clima por Id. Então teremos uma classe no namespace que a sua responsabilidade é buscar clima por Id.

Gosto de dizer que esta classe só pode ter um único método publico e caso seja necessário criar métodos complementares de auxilio ao objetivo do serviço estes métodos devem ser privados. Esta classe pode utilizar um validador, como busca em banco de dados deverá utilizar o repositório e por assim vai. Na pratica fica mais fácil de entender.

Então segue os serviços gerados:

public interface ICreateWeatherForecastService
{
    Task<GetWeatherForecastResponseContract> CreateWeatherForecastAsync(CreateWeatherForecastRequestContract request);
}

public class CreateWeatherForecastService : ICreateWeatherForecastService
{
    private readonly CreateWeatherForecastValidator _validator = new();
    private readonly IWeatherForecastRepository _weatherForecastRepository;

    public CreateWeatherForecastService(IWeatherForecastRepository weatherForecastRepository)
    {
        _weatherForecastRepository = weatherForecastRepository;
    }

    public async Task<GetWeatherForecastResponseContract> CreateWeatherForecastAsync(CreateWeatherForecastRequestContract request)
    {
        Validate(request);

        var weather = await CreateWeatherForecast(request);

        return CreateResponse(weather);
    }

    private void Validate(CreateWeatherForecastRequestContract request)
    {
        var validationResult = _validator.Validate(request);
        ValidationException.ThrowIf(!validationResult.IsValid, validationResult.Errors);
    }

    private async Task<WeatherForecast> CreateWeatherForecast(CreateWeatherForecastRequestContract request)
    {
        var weather = new WeatherForecast
        {
            Id = Guid.NewGuid(),
            Date = request.Date ?? DateTime.Now,
            TemperatureCelsius = request.TemperatureCelsius ?? default,
            Summary = request.Summary
        };

        await _weatherForecastRepository.AddAsync(weather);

        return weather;
    }

    private static GetWeatherForecastResponseContract CreateResponse(WeatherForecast weather)
    {
        return new GetWeatherForecastResponseContract
        {
            Id = weather.Id,
            Date = weather.Date,
            TemperatureCelsius = weather.TemperatureCelsius,
            Summary = weather.Summary
        };
    }
}
Enter fullscreen mode Exit fullscreen mode
public interface IDeleteWeatherForecastService
{
    Task DeleteWeatherForecastAsync(Guid? weatherForecastId);
}

public class DeleteWeatherForecastService : IDeleteWeatherForecastService
{
    private readonly IWeatherForecastRepository _weatherForecastRepository;

    public DeleteWeatherForecastService(IWeatherForecastRepository weatherForecastRepository)
    {
        _weatherForecastRepository = weatherForecastRepository;
    }

    public Task DeleteWeatherForecastAsync(Guid? weatherForecastId)
    {
        BadRequestException.ThrowIf(weatherForecastId is null, "Weather forecast identification is required.");

        return _weatherForecastRepository.DeleteAsync(weatherForecastId!.Value);
    }
}
Enter fullscreen mode Exit fullscreen mode

Para o método de buscar foi criado uma mecanismo para que seja possível efetuar consulta por meio de query string. Para isto foi necessário a elaboração de uma classe e um enumerador para fosse possível registrar o valor e o tipo de operação.

public enum Operation
{
    Equal,
    NotEqual,
    GreaterThan,
    GreaterThanOrEqual,
    LessThan,
    LessThanOrEqual,
    Contains,
    StartsWith,
    EndsWith
}

public sealed class OperationParam<T>
{
    public Operation Operation { get; set; }
    public T Value { get; set; } = default!;
}
Enter fullscreen mode Exit fullscreen mode
public interface IGetAllWeatherForecastsService
{
    Task<IEnumerable<GetWeatherForecastResponseContract>> GetAllWeatherForecastsAsync(string? param);
}

public class GetAllWeatherForecastsService : IGetAllWeatherForecastsService
{
    private readonly IWeatherForecastRepository _weatherForecastRepository;

    public GetAllWeatherForecastsService(IWeatherForecastRepository weatherForecastRepository)
    {
        _weatherForecastRepository = weatherForecastRepository;
    }

    public async Task<IEnumerable<GetWeatherForecastResponseContract>> GetAllWeatherForecastsAsync(string? param)
    {
        var date = param?.ExtractDateParam();
        var temperatureCelsius = param?.ExtractTemperatureCelsiusParam();
        var temperatureFahrenheit = param?.ExtractTemperatureFahrenheitParam();

        var weathers = _weatherForecastRepository.Get();
        weathers = FilterByDate(weathers, date);
        weathers = FilterByTemperatureCelsius(weathers, temperatureCelsius);
        weathers = FilterByTemperatureFahrenheit(weathers, temperatureFahrenheit);

        return await weathers.Select(x => new GetWeatherForecastResponseContract
            {
                Id = x.Id,
                Date = x.Date,
                TemperatureCelsius = x.TemperatureCelsius,
                Summary = x.Summary
            })
            .ToListAsync();
    }

    private static IQueryable<WeatherForecast> FilterByDate(IQueryable<WeatherForecast> weathers, OperationParam<DateTime>? filter)
    {
        return filter?.Operation switch
        {
            Operation.GreaterThan => weathers.Where(w => w.Date > filter.Value),
            Operation.LessThan => weathers.Where(w => w.Date < filter.Value),
            Operation.Equal => weathers.Where(w => w.Date == filter.Value),
            Operation.NotEqual => weathers.Where(w => w.Date != filter.Value),
            Operation.GreaterThanOrEqual => weathers.Where(w => w.Date >= filter.Value),
            Operation.LessThanOrEqual => weathers.Where(w => w.Date <= filter.Value),
            _ => weathers
        };
    }

    private static IQueryable<WeatherForecast> FilterByTemperatureCelsius(IQueryable<WeatherForecast> weathers, OperationParam<decimal>? filter)
    {
        return filter?.Operation switch
        {
            Operation.GreaterThan => weathers.Where(w => w.TemperatureCelsius > filter.Value),
            Operation.LessThan => weathers.Where(w => w.TemperatureCelsius < filter.Value),
            Operation.Equal => weathers.Where(w => w.TemperatureCelsius == filter.Value),
            Operation.NotEqual => weathers.Where(w => w.TemperatureCelsius != filter.Value),
            Operation.GreaterThanOrEqual => weathers.Where(w => w.TemperatureCelsius >= filter.Value),
            Operation.LessThanOrEqual => weathers.Where(w => w.TemperatureCelsius <= filter.Value),
            _ => weathers
        };
    }

    private static IQueryable<WeatherForecast> FilterByTemperatureFahrenheit(IQueryable<WeatherForecast> weathers, OperationParam<decimal>? filter)
    {
        return filter?.Operation switch
        {
            Operation.GreaterThan => weathers.Where(w => w.TemperatureCelsius.ToFahrenheit() > filter.Value),
            Operation.LessThan => weathers.Where(w => w.TemperatureCelsius.ToFahrenheit() < filter.Value),
            Operation.Equal => weathers.Where(w => w.TemperatureCelsius.ToFahrenheit() == filter.Value),
            Operation.NotEqual => weathers.Where(w => w.TemperatureCelsius.ToFahrenheit() != filter.Value),
            Operation.GreaterThanOrEqual => weathers.Where(w => w.TemperatureCelsius.ToFahrenheit() >= filter.Value),
            Operation.LessThanOrEqual => weathers.Where(w => w.TemperatureCelsius.ToFahrenheit() <= filter.Value),
            _ => weathers
        };
    }
}
Enter fullscreen mode Exit fullscreen mode
public interface IGetWeatherForecastService
{
    Task<GetWeatherForecastResponseContract> GetWeatherForecastAsync(Guid? weatherForecastId);
}

public class GetWeatherForecastService : IGetWeatherForecastService
{
    private readonly IWeatherForecastRepository _weatherForecastRepository;

    public GetWeatherForecastService(IWeatherForecastRepository weatherForecastRepository)
    {
        _weatherForecastRepository = weatherForecastRepository;
    }

    public async Task<GetWeatherForecastResponseContract> GetWeatherForecastAsync(Guid? weatherForecastId)
    {
        BadRequestException.ThrowIf(weatherForecastId is null, "Weather forecast identification is required.");

        var weather = await _weatherForecastRepository.GetAsync(weatherForecastId!.Value);

        NotFoundException.ThrowIf(weather is null, $"No weather forecast found by id {weatherForecastId}");

        return new GetWeatherForecastResponseContract
        {
            Id = weather!.Id,
            Date = weather.Date,
            TemperatureCelsius = weather.TemperatureCelsius,
            Summary = weather.Summary
        };
    }
}
Enter fullscreen mode Exit fullscreen mode
public interface IUpdateWeatherForecastService
{
    Task<GetWeatherForecastResponseContract> UpdateWeatherForecastAsync(Guid? weatherForecastId, UpdateWeatherForecastRequestContract request);
}

public class UpdateWeatherForecastService : IUpdateWeatherForecastService
{
    private readonly UpdateWeatherForecastValidator _validator = new();
    private readonly IWeatherForecastRepository _weatherForecastRepository;

    public UpdateWeatherForecastService(IWeatherForecastRepository weatherForecastRepository)
    {
        _weatherForecastRepository = weatherForecastRepository;
    }

    public async Task<GetWeatherForecastResponseContract> UpdateWeatherForecastAsync(Guid? weatherForecastId, UpdateWeatherForecastRequestContract request)
    {
        BadRequestException.ThrowIf(weatherForecastId != request.Id, "Weather forecast identification mismatch.");

        Validate(request);

        var weather = GetDatabaseWeatherForecast(request);

        await UpdateWeatherForecastAsync(weather);

        return CreateRespoonse(weather);
    }

    private void Validate(UpdateWeatherForecastRequestContract request)
    {
        var validationResult = _validator.Validate(request);
        ValidationException.ThrowIf(!validationResult.IsValid, validationResult.Errors);
    }

    private WeatherForecast GetDatabaseWeatherForecast(UpdateWeatherForecastRequestContract request)
    {
        var databaseWeatherForecast = _weatherForecastRepository.Get(request.Id!.Value);

        NotFoundException.ThrowIf(databaseWeatherForecast is null, "Weather forecast not found.");

        databaseWeatherForecast!.Date = request.Date ?? DateTime.Now;
        databaseWeatherForecast.Summary = request.Summary ?? databaseWeatherForecast.Summary;
        databaseWeatherForecast.TemperatureCelsius = request.TemperatureCelsius ?? databaseWeatherForecast.TemperatureCelsius;

        return databaseWeatherForecast;
    }

    private Task UpdateWeatherForecastAsync(WeatherForecast weatherForecast)
    {
        return _weatherForecastRepository.UpdateAsync(weatherForecast);
    }

    private static GetWeatherForecastResponseContract CreateRespoonse(WeatherForecast weatherForecast)
    {
        return new GetWeatherForecastResponseContract
        {
            Id = weatherForecast.Id,
            Date = weatherForecast.Date,
            Summary = weatherForecast.Summary,
            TemperatureCelsius = weatherForecast.TemperatureCelsius
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Projeto WebApi

Em relação a regras, validações e consultas vimos que foi elaborado todos os métodos onde um projeto somado ao anterior vai agregando valor para que este seja entregue ao usuário requisitando a operação. Tendo isto como premissa, o projeto de WebApi é apenas uma casca a ser utilizada como uma interface ao usuário.

É neste projeto que terá os métodos acessíveis ao usuário através da internet. Apesar de que este projeta seja apenas uma casca ele irá requerer algumas configurações para o seu devido funcionamento.

Padronização de erro

Então vamos começar pelas alterações feitas no Program.cs. Iremos configurar um comportamento na nossa API para todos os erros sejam reescrito para um objeto comum, a classe ErrorModel criada no projeto Shared.

  1. Devemos suprimir o comportamento existente da APi para que possamos incluir o nosso. Para isto incluímos o seguinte comando em Program.cs.
_ = builder.Services.Configure<ApiBehaviorOptions>(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
    });
Enter fullscreen mode Exit fullscreen mode
  1. Devemos criar o nosso padrão de tratamento de erro. Para isto devemos criar uma classe que herda o comportamento de ExceptionFilterAttribute. Aqui todas as vezes que nossa aplicação tiver uma exceção será executado o método OnException, então tratamos a exceção e enviamos o objeto resposta que desejamos. Observe que é aqui que se torna claro o principal motivo da criação de nossas exceções feitas no projeto Shared.
[AttributeUsage(AttributeTargets.All)]
public sealed class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
    private const string MediaType = "application/json";

    public override void OnException(ExceptionContext context)
    {
        object content = new { message = context.Exception.Message };
        HttpStatusCode code;
        switch (context.Exception.GetType().Name)
        {
            case nameof(BadHttpRequestException):
            case nameof(DbRegisterExistsException):
            case nameof(DeleteFailureException):
            case nameof(SaveFailureException):
                code = HttpStatusCode.BadRequest;
                break;

            case nameof(NotFoundException):
                code = HttpStatusCode.NotFound;
                break;

            case nameof(NotificationFailureException):
                code = HttpStatusCode.InternalServerError;
                break;

            case nameof(ValidationException):
                code = HttpStatusCode.NotAcceptable;
                content = ((ValidationException)context.Exception).Failures;
                break;

            default:
                code = HttpStatusCode.BadRequest;
                content = new
                {
                    message = "Something is wrong. Your Request could not be processed.",
                    exception = context.Exception.Message
                };
                break;
        }

        BuildContext(context, (int)code, content);
    }

    private static void BuildContext(ExceptionContext context, int code, object content)
    {
        context.HttpContext.Response.ContentType = MediaType;
        context.HttpContext.Response.StatusCode = code;
        var response = new ErrorModel
        {
            Code = code,
            Error = content,
            Exception = context.Exception.GetType().Name,
            StackTrace = context.Exception.StackTrace
        };

        context.Result = new ContentResult
        {
            StatusCode = code,
            ContentType = MediaType,
            Content = response.ToJson()
        };
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Com o filtro criado agora devemos adiciona-lo ao pipeline de nossa API. Em Program.cs Adicione o comando:
    _ = builder.Services.Configure<ApiBehaviorOptions>(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
    });

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add(typeof(CustomExceptionFilterAttribute));
        options.RespectBrowserAcceptHeader = true;
    });
Enter fullscreen mode Exit fullscreen mode

Em adicional vamos incluir uma configuração extra para a serialização da resposta da API. Nesta configuração iremos incluir para que os objetos com campo nulos não seja inclusos no JSON de resposta. Assim diminuímos a quantidade de dados enviamos e aumentamos a velocidade da resposta, em grande volume de requisição isto fará muita diferença.

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add(typeof(CustomExceptionFilterAttribute));
        options.RespectBrowserAcceptHeader = true;
    })
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
        options.JsonSerializerOptions.WriteIndented = false;
    });
Enter fullscreen mode Exit fullscreen mode
  1. Incluímos o filtro para a padronização mas nem todas as exceções passam pelo filtro. Não irei entrar em detalhes no motivo mas caso tenha interesse recomendo um estudo sobre o fluxo do pipeline do WebApi. Voltando a nossa aplicação, para garantir 100% do tratamento também iremos criar um middleware para as requisições.
public sealed class RequestHandlerMiddleware
{
    private const string MediaType = "application/json";
    private const string UnauthorizedMessage = "Unauthorized access";
    private const string DefaultErrorMessage = "An error occurred while processing your request";
    private readonly RequestDelegate _next;

    public RequestHandlerMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);

            if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized)
                throw new UnauthorizedAccessException(UnauthorizedMessage);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception? exception)
    {
        var code = exception switch
        {
            BadRequestException => HttpStatusCode.BadRequest,
            NotFoundException => HttpStatusCode.NotFound,
            DeleteFailureException => HttpStatusCode.BadRequest,
            NotificationFailureException => HttpStatusCode.BadRequest,
            SaveFailureException => HttpStatusCode.BadRequest,
            ValidationException => HttpStatusCode.BadRequest,
            _ => HttpStatusCode.InternalServerError
        };

        context.Response.ContentType = MediaType;
        context.Response.StatusCode = (int)code;
        var error = new ErrorModel
        {
            Code = (int)code,
            Error = exception?.Message ?? DefaultErrorMessage,
            Exception = exception?.GetType().Name,
            StackTrace = exception?.StackTrace
        };

        return context.Response.WriteAsync(error.ToJson());
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Agora inclusos o middleware em nosso fluxo. No Program.cs incluimos o comando:
var app = builder.Build();

app.UseMiddleware<RequestHandlerMiddleware>();
Enter fullscreen mode Exit fullscreen mode

Injetando dependências na aplicação

Sem entrar em descrição técnica sobre o que é injeção de dependência e ao mesmo tempo colocando em um formato ultra simplificado, injeção de dependência é você incluir no contexto de sua aplicação classe que poderão ser utilizadas durante a execução de alguma operação sem se preocupar com o que irá acontecer com ela posteriormente e/ou como ela é gerada.

Para maiores informações a respeito leiam (Injeção de dependência no ASP.NET Core)(https://learn.microsoft.com/pt-br/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2) e Porque devemos utilizar a injeção de dependência no .NET ?.

Para injetar uma dependência basta incluir o comando em Program.cs:

    services.AddDbContext<ApiTemplateContext>(o => o.UseInMemoryDatabase("ApiTemplate"));
    services.AddScoped<IWeatherForecastRepository, WeatherForecastRepository>();
    services.AddScoped<ICreateWeatherForecastService, CreateWeatherForecastService>();
    services.AddScoped<IDeleteWeatherForecastService, DeleteWeatherForecastService>();
    services.AddScoped<IGetWeatherForecastService, GetWeatherForecastService>();
    services.AddScoped<IGetAllWeatherForecastsService, GetAllWeatherForecastsService>();
    services.AddScoped<IUpdateWeatherForecastService, UpdateWeatherForecastService>();

    WebApplication app = builder.Build();
Enter fullscreen mode Exit fullscreen mode

Controller

Resumidamente, controller é onde fica o(s) método(s) com acesso pela internet. Na criação do projeto WebApi o template entrega WeatherForecastControlle.cs que é criado pelo próprio .NET. Agora iremos modifica-lo para atender tudo que fizemos anteriormente.

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    /// <summary>
    ///     List all weather forecasts
    /// </summary>
    /// <remarks>
    ///     For query use: field=[operationType,value]
    ///
    ///     Example: date=[GreaterThan,2019-01-01]
    ///
    ///     OperationType:
    ///     Equal,
    ///     NotEqual,
    ///     GreaterThan,
    ///     GreaterThanOrEqual,
    ///     LessThan,
    ///     LessThanOrEqual
    /// </remarks>
    /// <param name="service"></param>
    /// <param name="param"></param>
    /// <returns></returns>
    [HttpGet]
    [ProducesResponseType(typeof(IEnumerable<GetWeatherForecastResponseContract>), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<IEnumerable<GetWeatherForecastResponseContract>>> GetAsync([FromServices] IGetAllWeatherForecastsService service, string? param = null)
    {
        var result = await service.GetAllWeatherForecastsAsync(param);
        if (!result.Any())
            return NoContent();

        return Ok(result);
    }

    /// <summary>
    /// Get a specific weather forecast
    /// </summary>
    /// <param name="service"></param>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpGet("{id}")]
    [ProducesResponseType(typeof(GetWeatherForecastResponseContract), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<GetWeatherForecastResponseContract>> GetASync([FromServices] IGetWeatherForecastService service, Guid? id)
    {
        return Ok(await service.GetWeatherForecastAsync(id));
    }

    /// <summary>
    ///   Create a new weather forecast
    /// </summary>
    /// <param name="service"></param>
    /// <param name="request"></param>
    /// <returns></returns>
    [HttpPost]
    [ProducesResponseType(typeof(GetWeatherForecastResponseContract), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<GetWeatherForecastResponseContract>> PostAsync([FromServices] ICreateWeatherForecastService service,
        [FromBody] CreateWeatherForecastRequestContract request)
    {
        var result = await service.CreateWeatherForecastAsync(request);
        return Created("weatherforecast", result);
    }

    /// <summary>
    ///  Update a weather forecast
    /// </summary>
    /// <param name="service"></param>
    /// <param name="id"></param>
    /// <param name="request"></param>
    /// <returns></returns>
    [HttpPut("{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)]
    public async Task<ActionResult> PutAsync([FromServices] IUpdateWeatherForecastService service, Guid id, [FromBody] UpdateWeatherForecastRequestContract request)
    {
        _ = await service.UpdateWeatherForecastAsync(id, request);
        return NoContent();
    }

    /// <summary>
    ///  Delete a weather forecast
    /// </summary>
    /// <param name="service"></param>
    /// <param name="id"></param>
    [HttpDelete("{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)]
    public async Task<ActionResult> DeleteAsync([FromServices] IDeleteWeatherForecastService service, Guid? id)
    {
        await service.DeleteWeatherForecastAsync(id);
        return NoContent();
    }
}
Enter fullscreen mode Exit fullscreen mode

Na controller que apresentei não contém construtor onde passo as instâncias de classes necessárias para os métodos subsequentes. Isto porque cada método tem uma dependência diferente e se for passado no construtor ao executar o método Get irei instanciar as dependências de todos os métodos.

Então, como já injeto as dependências na aplicação. Então basta informar na declaração do método que para execução deve ser obtido uma dependência contida na aplicação. Para fazer isto na declaração do método adiciono a seguinte informação '[FromService]' seguido pelo nome da classe que necessito e a declaração da variável para o uso.

public async Task<ActionResult<IEnumerable<GetWeatherForecastResponseContract>>> GetAsync([FromServices] IGetAllWeatherForecastsService service, string? param = null)
Enter fullscreen mode Exit fullscreen mode

Então quando o método for executado apenas aquela dependência irá ser instanciada.

Acima de cada método existe uma TAG XML com summary, o texto que estiver dentro da TAG irá ser apresentado no Swagger como a descrição do método.

A TAG '[ProducesResponseType(StatusCodes.Status204NoContent)]' irá mostrar no swagger que o método pode apresentar o código HTTP 204.

A TAG '[ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)]' irá mostrar no swagger que o método pode apresentar o código HTTP 400 retornando o objeto do tipo ErrorModel.

Bom, imagino que conseguiram entender a lógica das TAG vinculadas aos métodos. Claro que um melhor entendimento é sempre aconselhável a leitura da documentação técnica dos componentes utilizados assim como a documentação do próprio .NET.

Chegamos ao fim desta breve longa postagem. Sei que alguns ponto poderiam ser melhor explicado mas deixo o código fonte disponível em meu github e qualquer duvida deixe um comentário que estarei respondendo assim que possível ou mesmo criando uma nova postagem abordando algo especifico.

💖 💪 🙅 🚩
rodbarbosa
Rod Barbosa

Posted on November 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Criando API com .NET
webdev Criando API com .NET

November 6, 2022