Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/Api/Controllers/SampleController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ public SampleController(ISampleService sampleService)
/// </summary>
/// <returns>The sample response model.</returns>
[HttpGet]
public async Task<IActionResult> Get()
public IActionResult Get()
{
var result = await this.sampleService.GetSamplesAsync(new GetSamplesRequestModel(), CancellationToken.None);
var result = this.sampleService.GetSamples(new GetSamplesRequestModel());

return this.Ok(result);
}

Expand All @@ -45,10 +46,6 @@ public async Task<IActionResult> Get()
public async Task<IActionResult> Get(Guid id)
{
var result = await this.sampleService.GetSampleAsync(id, CancellationToken.None);
if (result == null)
{
return this.NotFound();
}

return this.Ok(result);
}
Expand All @@ -61,12 +58,13 @@ public async Task<IActionResult> Get(Guid id)
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateSampleRequestModel request)
{
if (request == null)
if (request is null)
{
return this.BadRequest("Request cannot be null.");
}

var result = await this.sampleService.CreateSampleAsync(request, CancellationToken.None);

return this.CreatedAtAction(nameof(this.Get), new { id = result.Id }, result);
}

Expand All @@ -78,13 +76,14 @@ public async Task<IActionResult> Create([FromBody] CreateSampleRequestModel requ
[HttpPut]
public async Task<IActionResult> Update([FromBody] UpdateSampleRequestModel request)
{
if (request == null || request.Id == Guid.Empty)
if (request is null || request.Id == Guid.Empty)
{
return this.BadRequest("Request cannot be null and ID must be provided.");
}

var result = await this.sampleService.UpdateSampleAsync(request, CancellationToken.None);
if (result == null)

if (result is null)
{
return this.NotFound();
}
Expand All @@ -106,6 +105,7 @@ public async Task<IActionResult> Delete(Guid id)
}

var result = await this.sampleService.DeleteSampleAsync(id, CancellationToken.None);

if (!result)
{
return this.NotFound();
Expand Down
1 change: 1 addition & 0 deletions src/Api/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
*/
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileHeaderFileNameDocumentationMustMatchTypeName", Justification = "C# 9.0 top-level statements do not require a namespace or a class.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:FileMustHaveHeader", Justification = "Reviewed.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Reviewed.")]
84 changes: 84 additions & 0 deletions src/Api/Middlewares/ExceptionMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
namespace Api.Middlewares
{
using System;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Domain.Exceptions;
using Microsoft.AspNetCore.Http;

/// <summary>
/// Extension methods for configuring the exception handling middleware.
/// </summary>
public static class ExceptionHandlingExtensions
{
/// <summary>
/// Adds the global exception handler middleware to the application pipeline.
/// </summary>
/// <param name="app">The application builder.</param>
/// <returns>The application builder with the middleware added.</returns>
public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app)
=> app.UseMiddleware<ExceptionHandlingMiddleware>();
}

/// <summary>
/// Middleware to handle exceptions globally in the application.
/// </summary>
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate next;

/// <summary>
/// Initializes a new instance of the <see cref="ExceptionHandlingMiddleware"/> class.
/// </summary>
/// <param name="next">The next middleware in the pipeline.</param>
public ExceptionHandlingMiddleware(RequestDelegate next)
=> this.next = next;

/// <summary>
/// Invokes the middleware to handle exceptions.
/// </summary>
/// <param name="ctx">The HTTP context.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task InvokeAsync(HttpContext ctx)
{
try
{
await this.next(ctx);
}
catch (Exception ex)
{
await HandleExceptionAsync(ctx, ex);
}
}

private static Task HandleExceptionAsync(HttpContext ctx, Exception exception)
{
HttpStatusCode status;
string errorCode;
string message = exception.Message;

if (exception is DomainException de)
{
status = de.StatusCode;
errorCode = de.ErrorCode;
}
else
{
status = HttpStatusCode.InternalServerError;
errorCode = "UNHANDLED_ERROR";
}

ctx.Response.ContentType = "application/json";
ctx.Response.StatusCode = (int)status;

var payload = JsonSerializer.Serialize(new
{
error = message,
code = errorCode,
});

return ctx.Response.WriteAsync(payload);
}
}
}
2 changes: 2 additions & 0 deletions src/Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@
.WithApiVersionSet(new ApiVersionSet(new ApiVersionSetBuilder(string.Empty), "weatherForecast"))
.HasApiVersion(new ApiVersion(2, 0));

app.UseGlobalExceptionHandler();

app.MapControllers();
app.MapMetrics();

Expand Down
6 changes: 6 additions & 0 deletions src/Domain/Enums/SortDirection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Domain.Enums;
public enum SortDirection
{
Asc,
Desc
}
18 changes: 18 additions & 0 deletions src/Domain/Exceptions/DomainException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Net;

namespace Domain.Exceptions
{
public class DomainException : Exception
{
public string ErrorCode { get; }
public HttpStatusCode StatusCode { get; }
public DomainException(string message,
string errorCode = "DOMAIN_ERROR",
HttpStatusCode statusCode = HttpStatusCode.BadRequest)
: base(message)
{
ErrorCode = errorCode;
StatusCode = statusCode;
}
}
}
2 changes: 2 additions & 0 deletions src/Domain/Interfaces/IRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@ Task<int> ExecuteUpdateAsync(
CancellationToken cancellationToken = default);

Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);

object Property(TEntity entity, string propertyName);
}
4 changes: 2 additions & 2 deletions src/Domain/Interfaces/ISampleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ namespace Domain.Interfaces;

public interface ISampleService
{
GetSamplesResponseModel GetSamples(GetSamplesRequestModel request);
Task<GetSampleResponseModel> GetSampleAsync(Guid id, CancellationToken cancellationToken = default);
Task<CreateSampleResponseModel> CreateSampleAsync(CreateSampleRequestModel request, CancellationToken cancellationToken = default);
Task<GetSamplesResponseModel> GetSamplesAsync(GetSamplesRequestModel request, CancellationToken cancellationToken = default);
Task<UpdateSampleResponseModel> UpdateSampleAsync(UpdateSampleRequestModel request, CancellationToken cancellationToken = default);
Task<GetSampleResponseModel> GetSampleAsync(Guid id, CancellationToken cancellationToken = default);
Task<bool> DeleteSampleAsync(Guid id, CancellationToken cancellationToken = default);
}
11 changes: 11 additions & 0 deletions src/Domain/Models/Request/FilterRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Domain.Entities;
using Domain.Enums;

namespace Domain.Models.Request;
public class FilterRequestModel
{
public int Page { get; set; } = 1;
public int Limit { get; set; } = 10;
public string OrderBy { get; set; } = nameof(BaseEntity<Guid>.Id);
public SortDirection Direction { get; set; } = SortDirection.Asc;
}
2 changes: 1 addition & 1 deletion src/Domain/Models/Request/Sample/GetSamplesRequestModel.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Domain.Models.Request.Sample;

public class GetSamplesRequestModel
public class GetSamplesRequestModel: FilterRequestModel
{
public string? Name { get; set; }
}
14 changes: 13 additions & 1 deletion src/Domain/Models/Response/Sample/GetSamplesResponseModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,17 @@ namespace Domain.Models.Response.Sample;

public class GetSamplesResponseModel
{
public IList<GetSampleResponseModel> Samples { get; set; } = new List<GetSampleResponseModel>();
public IReadOnlyList<GetSampleResponseModel> Items { get; }
public int TotalCount { get; }
public int TotalPages { get; }

public GetSamplesResponseModel(
IReadOnlyList<GetSampleResponseModel> items,
int totalCount,
int totalPages)
{
Items = items;
TotalCount = totalCount;
TotalPages = totalPages;
}
}
46 changes: 33 additions & 13 deletions src/Domain/Services/SampleService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Linq.Expressions;
using System.Threading.Tasks;
using AutoMapper;
using Domain.Entities;
using Domain.Enums;
using Domain.Exceptions;
using Domain.Interfaces;
using Domain.Models.Request.Sample;
using Domain.Models.Response.Sample;
Expand All @@ -22,24 +22,44 @@ public SampleService(IRepository<SampleEntity> repository,

public async Task<GetSampleResponseModel> GetSampleAsync(Guid id, CancellationToken cancellationToken = default)
{
var entity = await _repository.GetByIdAsync(id, cancellationToken);
var entity = await _repository.GetByIdAsync(id, cancellationToken) ?? throw new DomainException($"Sample with ID {id} not found.");

return _mapper.Map<GetSampleResponseModel>(entity);
}

public async Task<GetSamplesResponseModel> GetSamplesAsync(GetSamplesRequestModel request, CancellationToken cancellationToken = default)
public GetSamplesResponseModel GetSamples(GetSamplesRequestModel request)
{
Expression<Func<SampleEntity, bool>> predicate = e => true;
if (!string.IsNullOrEmpty(request.Name))
{
predicate = e => e.Name == request.Name;
var query = _repository.Query();

if (!string.IsNullOrWhiteSpace(request.Name))
query = query.Where(u => u.Name == request.Name);

if (!string.IsNullOrWhiteSpace(request.OrderBy))
{
query = request.Direction == SortDirection.Desc
? query.OrderByDescending(u => _repository.Property(u, request.OrderBy))
: query.OrderBy(u => _repository.Property(u, request.OrderBy));
}
var samples = await _repository.ListAsync(predicate, true, cancellationToken);
var response = new GetSamplesResponseModel
else
{
Samples = _mapper.Map<List<GetSampleResponseModel>>(samples)
};
return response;
query = query.OrderByDescending(u => _repository.Property(u, nameof(SampleEntity.CreatedAt)));
}

var totalCount = query.Count();

var skip = (request.Page - 1) * request.Limit;
var samples = query
.Skip(skip)
.Take(request.Limit)
.ToList();

var items = samples
.Select(u => _mapper.Map<GetSampleResponseModel>(u))
.ToList();

var totalPages = (int)Math.Ceiling(totalCount / (double)request.Limit);

return new GetSamplesResponseModel(items, totalCount, totalPages);
}

public async Task<CreateSampleResponseModel> CreateSampleAsync(CreateSampleRequestModel request, CancellationToken cancellationToken = default)
Expand Down
5 changes: 5 additions & 0 deletions src/Infra/Repositories/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ public Task<int> ExecuteUpdateAsync(

public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) => _dataContext.SaveChangesAsync(cancellationToken);

public object Property(TEntity entity, string propertyName)
{
return EF.Property<object>(entity, propertyName);
}

public async ValueTask DisposeAsync()
{
if (Interlocked.Exchange(ref _disposed, 1) == 0)
Expand Down