diff --git a/src/Api/Controllers/SampleController.cs b/src/Api/Controllers/SampleController.cs
index 5f7665d..26cae65 100644
--- a/src/Api/Controllers/SampleController.cs
+++ b/src/Api/Controllers/SampleController.cs
@@ -30,9 +30,10 @@ public SampleController(ISampleService sampleService)
///
/// The sample response model.
[HttpGet]
- public async Task 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);
}
@@ -45,10 +46,6 @@ public async Task Get()
public async Task Get(Guid id)
{
var result = await this.sampleService.GetSampleAsync(id, CancellationToken.None);
- if (result == null)
- {
- return this.NotFound();
- }
return this.Ok(result);
}
@@ -61,12 +58,13 @@ public async Task Get(Guid id)
[HttpPost]
public async Task 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);
}
@@ -78,13 +76,14 @@ public async Task Create([FromBody] CreateSampleRequestModel requ
[HttpPut]
public async Task 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();
}
@@ -106,6 +105,7 @@ public async Task Delete(Guid id)
}
var result = await this.sampleService.DeleteSampleAsync(id, CancellationToken.None);
+
if (!result)
{
return this.NotFound();
diff --git a/src/Api/GlobalSuppressions.cs b/src/Api/GlobalSuppressions.cs
index 6cc98eb..2fef874 100644
--- a/src/Api/GlobalSuppressions.cs
+++ b/src/Api/GlobalSuppressions.cs
@@ -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.")]
\ No newline at end of file
diff --git a/src/Api/Middlewares/ExceptionMiddleware.cs b/src/Api/Middlewares/ExceptionMiddleware.cs
new file mode 100644
index 0000000..aca7d59
--- /dev/null
+++ b/src/Api/Middlewares/ExceptionMiddleware.cs
@@ -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;
+
+ ///
+ /// Extension methods for configuring the exception handling middleware.
+ ///
+ public static class ExceptionHandlingExtensions
+ {
+ ///
+ /// Adds the global exception handler middleware to the application pipeline.
+ ///
+ /// The application builder.
+ /// The application builder with the middleware added.
+ public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app)
+ => app.UseMiddleware();
+ }
+
+ ///
+ /// Middleware to handle exceptions globally in the application.
+ ///
+ public class ExceptionHandlingMiddleware
+ {
+ private readonly RequestDelegate next;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The next middleware in the pipeline.
+ public ExceptionHandlingMiddleware(RequestDelegate next)
+ => this.next = next;
+
+ ///
+ /// Invokes the middleware to handle exceptions.
+ ///
+ /// The HTTP context.
+ /// A task representing the asynchronous operation.
+ 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);
+ }
+ }
+}
diff --git a/src/Api/Program.cs b/src/Api/Program.cs
index 71d9d65..ab2535b 100644
--- a/src/Api/Program.cs
+++ b/src/Api/Program.cs
@@ -90,6 +90,8 @@
.WithApiVersionSet(new ApiVersionSet(new ApiVersionSetBuilder(string.Empty), "weatherForecast"))
.HasApiVersion(new ApiVersion(2, 0));
+app.UseGlobalExceptionHandler();
+
app.MapControllers();
app.MapMetrics();
diff --git a/src/Domain/Enums/SortDirection.cs b/src/Domain/Enums/SortDirection.cs
new file mode 100644
index 0000000..6f8e747
--- /dev/null
+++ b/src/Domain/Enums/SortDirection.cs
@@ -0,0 +1,6 @@
+namespace Domain.Enums;
+public enum SortDirection
+{
+ Asc,
+ Desc
+}
\ No newline at end of file
diff --git a/src/Domain/Exceptions/DomainException.cs b/src/Domain/Exceptions/DomainException.cs
new file mode 100644
index 0000000..94c2f0c
--- /dev/null
+++ b/src/Domain/Exceptions/DomainException.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/Domain/Interfaces/IRepository.cs b/src/Domain/Interfaces/IRepository.cs
index eedf958..6c8a49b 100644
--- a/src/Domain/Interfaces/IRepository.cs
+++ b/src/Domain/Interfaces/IRepository.cs
@@ -54,4 +54,6 @@ Task ExecuteUpdateAsync(
CancellationToken cancellationToken = default);
Task SaveChangesAsync(CancellationToken cancellationToken = default);
+
+ object Property(TEntity entity, string propertyName);
}
\ No newline at end of file
diff --git a/src/Domain/Interfaces/ISampleService.cs b/src/Domain/Interfaces/ISampleService.cs
index 5a731dd..5b7012e 100644
--- a/src/Domain/Interfaces/ISampleService.cs
+++ b/src/Domain/Interfaces/ISampleService.cs
@@ -5,9 +5,9 @@ namespace Domain.Interfaces;
public interface ISampleService
{
+ GetSamplesResponseModel GetSamples(GetSamplesRequestModel request);
+ Task GetSampleAsync(Guid id, CancellationToken cancellationToken = default);
Task CreateSampleAsync(CreateSampleRequestModel request, CancellationToken cancellationToken = default);
- Task GetSamplesAsync(GetSamplesRequestModel request, CancellationToken cancellationToken = default);
Task UpdateSampleAsync(UpdateSampleRequestModel request, CancellationToken cancellationToken = default);
- Task GetSampleAsync(Guid id, CancellationToken cancellationToken = default);
Task DeleteSampleAsync(Guid id, CancellationToken cancellationToken = default);
}
diff --git a/src/Domain/Models/Request/FilterRequestModel.cs b/src/Domain/Models/Request/FilterRequestModel.cs
new file mode 100644
index 0000000..eea8d1b
--- /dev/null
+++ b/src/Domain/Models/Request/FilterRequestModel.cs
@@ -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.Id);
+ public SortDirection Direction { get; set; } = SortDirection.Asc;
+}
\ No newline at end of file
diff --git a/src/Domain/Models/Request/Sample/GetSamplesRequestModel.cs b/src/Domain/Models/Request/Sample/GetSamplesRequestModel.cs
index b8d9965..5d4e453 100644
--- a/src/Domain/Models/Request/Sample/GetSamplesRequestModel.cs
+++ b/src/Domain/Models/Request/Sample/GetSamplesRequestModel.cs
@@ -1,6 +1,6 @@
namespace Domain.Models.Request.Sample;
-public class GetSamplesRequestModel
+public class GetSamplesRequestModel: FilterRequestModel
{
public string? Name { get; set; }
}
diff --git a/src/Domain/Models/Response/Sample/GetSamplesResponseModel.cs b/src/Domain/Models/Response/Sample/GetSamplesResponseModel.cs
index 47e747a..7937a06 100644
--- a/src/Domain/Models/Response/Sample/GetSamplesResponseModel.cs
+++ b/src/Domain/Models/Response/Sample/GetSamplesResponseModel.cs
@@ -2,5 +2,17 @@ namespace Domain.Models.Response.Sample;
public class GetSamplesResponseModel
{
- public IList Samples { get; set; } = new List();
+ public IReadOnlyList Items { get; }
+ public int TotalCount { get; }
+ public int TotalPages { get; }
+
+ public GetSamplesResponseModel(
+ IReadOnlyList items,
+ int totalCount,
+ int totalPages)
+ {
+ Items = items;
+ TotalCount = totalCount;
+ TotalPages = totalPages;
+ }
}
diff --git a/src/Domain/Services/SampleService.cs b/src/Domain/Services/SampleService.cs
index ca07584..ca90113 100644
--- a/src/Domain/Services/SampleService.cs
+++ b/src/Domain/Services/SampleService.cs
@@ -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;
@@ -22,24 +22,44 @@ public SampleService(IRepository repository,
public async Task 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(entity);
}
- public async Task GetSamplesAsync(GetSamplesRequestModel request, CancellationToken cancellationToken = default)
+ public GetSamplesResponseModel GetSamples(GetSamplesRequestModel request)
{
- Expression> 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>(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(u))
+ .ToList();
+
+ var totalPages = (int)Math.Ceiling(totalCount / (double)request.Limit);
+
+ return new GetSamplesResponseModel(items, totalCount, totalPages);
}
public async Task CreateSampleAsync(CreateSampleRequestModel request, CancellationToken cancellationToken = default)
diff --git a/src/Infra/Repositories/Repository.cs b/src/Infra/Repositories/Repository.cs
index 4f82710..fba35c9 100644
--- a/src/Infra/Repositories/Repository.cs
+++ b/src/Infra/Repositories/Repository.cs
@@ -109,6 +109,11 @@ public Task ExecuteUpdateAsync(
public Task SaveChangesAsync(CancellationToken cancellationToken = default) => _dataContext.SaveChangesAsync(cancellationToken);
+ public object Property(TEntity entity, string propertyName)
+ {
+ return EF.Property