Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,4 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml
/.idea
27 changes: 23 additions & 4 deletions src/ThinkCoreBE.Api/Controllers/CustomerController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using ThinkCoreBE.Application.Interfaces;
using Microsoft.AspNetCore.Mvc;
using ThinkCoreBE.Application;
using ThinkCoreBE.Application.Interfaces;
using ThinkCoreBE.Domain.Entities;

namespace ThinkCoreBE.Api.Controllers
{
Expand All @@ -7,15 +10,31 @@ public static class CustomerController
public static void AddCustomerEndpoints(this WebApplication app)
{
app.MapGet("/customers/getAll", GetAllCustomers)
.AddEndpointFilter<ResultEndpointFilter>()
.WithName("GetAllCustomers")
.WithOpenApi();

app.MapDelete("/customers/deleteById", DeleteCustomerById)
.AddEndpointFilter<ResultEndpointFilter>()
.WithName("DeleteCustomerById")
.WithOpenApi();
}

private static async Task<IResult> GetAllCustomers(ICustomerService customerService, CancellationToken cancellationToken)
private static async Task<Result<IEnumerable<Customer>>> GetAllCustomers(
ICustomerService customerService,
CancellationToken cancellationToken)
{
var customers = await customerService.GetAllCustomersAsync(cancellationToken);
return Results.Ok(customers);
// Return a Result<T> (the filter handles final HTTP mapping)
return await customerService.GetAllCustomersAsync(cancellationToken);
}

private static async Task<Result<string>> DeleteCustomerById(
[FromQuery] long id,
ICustomerService customerService,
CancellationToken cancellationToken)
{
// Returns a simple success/fail message
return await customerService.DeleteCustomerByIdAsync(id, cancellationToken);
}
}
}
3 changes: 3 additions & 0 deletions src/ThinkCoreBE.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using FluentMigrator.Runner;
using Microsoft.AspNetCore.Authentication.Negotiate;
using ThinkCoreBE.Api;
using ThinkCoreBE.Api.Controllers;
using ThinkCoreBE.Application;
using ThinkCoreBE.Infrastructure;
Expand All @@ -17,6 +18,8 @@
options.FallbackPolicy = options.DefaultPolicy;
});

builder.Services.AddScoped<ResultEndpointFilter>();

builder.Services.AddFluentMigratorCore()
.ConfigureRunner(runner => runner
.AddPostgres()
Expand Down
53 changes: 53 additions & 0 deletions src/ThinkCoreBE.Api/ResultEndpointFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using ThinkCoreBE.Application;

namespace ThinkCoreBE.Api
{
// An IEndpointFilter that converts a Result<T> object returned from a minimal API endpoint into an HTTP IResult.
public class ResultEndpointFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
// Execute the endpoint (or next filter) and capture the result
var methodResult = await next(context);

// If the endpoint already returns IResult, no extra handling needed
if (methodResult is IResult directIResult)
return directIResult;

// If the endpoint result is a Result<T>, map it to IResult
if (methodResult != null && IsResultOfT(methodResult))
{
return ConvertResultToIResult(methodResult);
}

// If it's neither, return as-is
return methodResult;
}

private static bool IsResultOfT(object obj)
{
var type = obj.GetType();
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Result<>);
}

private static IResult ConvertResultToIResult(object resultObj)
{
var type = resultObj.GetType();

bool? success = type.GetProperty(nameof(Result<object>.Success))
?.GetValue(resultObj) as bool?;
if (success == true)
{
var content = type.GetProperty(nameof(Result<object>.Content))?.GetValue(resultObj);
return Results.Ok(content);
}
else
{
var errorMsg = type.GetProperty(nameof(Result<object>.ErrorMessage))?.GetValue(resultObj) as string;
return Results.NotFound(new { Error = errorMsg });
}
}
}
}
4 changes: 3 additions & 1 deletion src/ThinkCoreBE.Application/Interfaces/ICustomerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace ThinkCoreBE.Application.Interfaces
{
public interface ICustomerService
{
public Task<IEnumerable<Customer>> GetAllCustomersAsync(CancellationToken cancellationToken);
public Task<Result<IEnumerable<Customer>>> GetAllCustomersAsync(CancellationToken cancellationToken);

public Task<Result<string>> DeleteCustomerByIdAsync(long id, CancellationToken cancellationToken);
}
}
17 changes: 17 additions & 0 deletions src/ThinkCoreBE.Application/Result.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace ThinkCoreBE.Application
{
// Generic result wrapper for service-layer operations.
/// <typeparam name="T">Type of data returned by the service.</typeparam>
public class Result<T> //
{
public bool Success { get; set; }

public T? Content { get; set; }

public string? ErrorMessage { get; set; }

public static Result<T> Ok(T content) => new() { Success = true, Content = content };

public static Result<T> Fail(string errorMessage) => new() { Success = false, ErrorMessage = errorMessage };
}
}
34 changes: 32 additions & 2 deletions src/ThinkCoreBE.Application/Services/CustomerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,39 @@ public sealed class CustomerService : ICustomerService

public CustomerService(IThinkCoreDbContext context) { _context = context; }

public async Task<IEnumerable<Customer>> GetAllCustomersAsync(CancellationToken cancellationToken = default)
public async Task<Result<IEnumerable<Customer>>> GetAllCustomersAsync(CancellationToken cancellationToken = default)
{
return await _context.Customers.GetAllAsync(cancellationToken);
try
{
var customers = await _context.Customers.GetAllAsync(cancellationToken);
return Result<IEnumerable<Customer>>.Ok(customers);
}
catch (Exception ex)
{
// Log if needed
return Result<IEnumerable<Customer>>.Fail($"Error fetching customers: {ex.Message}");
}
}

public async Task<Result<string>> DeleteCustomerByIdAsync(long id, CancellationToken cancellationToken = default)
{
try
{
var affectedRows = await _context.Customers.DeleteByIdAsync(id, cancellationToken);
if (affectedRows > 0)
{
return Result<string>.Ok("Customer deleted successfully.");
}
else
{
return Result<string>.Fail("Customer not found");
}
}
catch (Exception ex)
{
// Log if needed
return Result<string>.Fail($"Error deleting customer: {ex.Message}");
}
}
}
}
2 changes: 2 additions & 0 deletions src/ThinkCoreBE.Domain/Interfaces/IDapperEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
public interface IDapperEntity<T>
{
public Task<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken);

public Task<int> DeleteByIdAsync(long id, CancellationToken cancellationToken);
}
}
7 changes: 7 additions & 0 deletions src/ThinkCoreBE.Infrastructure/Persistance/DapperEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,12 @@ public async Task<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToke
var sql = $"SELECT * FROM \"{typeof(T).Name}s\"";
return await _connection.QueryAsync<T>(sql, cancellationToken);
}

public async Task<int> DeleteByIdAsync(long id, CancellationToken cancellationToken = default)
{
var sql = $"DELETE FROM \"{typeof(T).Name}s\" WHERE \"{typeof(T).Name}Id\" = @Id";
var affectedRows = await Task.Run(async () => await _connection.ExecuteAsync(sql, new { Id = id }), cancellationToken);
return affectedRows;
}
}
}