Skip to content

Commit

Permalink
(#28) webApi: add library
Browse files Browse the repository at this point in the history
  • Loading branch information
SaintAngeLs committed Sep 21, 2024
1 parent 8fbd108 commit bf55b75
Show file tree
Hide file tree
Showing 19 changed files with 1,234 additions and 0 deletions.
230 changes: 230 additions & 0 deletions src/Paralax.WebApi/src/EndpointsBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Paralax.WebApi.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Paralax.WebApi
{
public class EndpointsBuilder : IEndpointsBuilder
{
private readonly WebApiEndpointDefinitions _definitions;
private readonly IEndpointRouteBuilder _routeBuilder;

public EndpointsBuilder(IEndpointRouteBuilder routeBuilder, WebApiEndpointDefinitions definitions)
{
_routeBuilder = routeBuilder;
_definitions = definitions;
}

public IEndpointsBuilder Get(string path, Func<HttpContext, Task> context = null,
Action<IEndpointConventionBuilder> endpoint = null, bool auth = false, string roles = null,
params string[] policies)
{
var builder = _routeBuilder.MapGet(path, ctx => context?.Invoke(ctx));
endpoint?.Invoke(builder);
ApplyAuthRolesAndPolicies(builder, auth, roles, policies);
AddEndpointDefinition(HttpMethods.Get, path);

return this;
}

public IEndpointsBuilder Get<T>(string path, Func<T, HttpContext, Task> context = null,
Action<IEndpointConventionBuilder> endpoint = null, bool auth = false, string roles = null,
params string[] policies) where T : class
{
var builder = _routeBuilder.MapGet(path, ctx => BuildQueryContext(ctx, context));
endpoint?.Invoke(builder);
ApplyAuthRolesAndPolicies(builder, auth, roles, policies);
AddEndpointDefinition<T>(HttpMethods.Get, path);

return this;
}

public IEndpointsBuilder Get<TRequest, TResult>(string path, Func<TRequest, HttpContext, Task> context = null,
Action<IEndpointConventionBuilder> endpoint = null, bool auth = false, string roles = null,
params string[] policies) where TRequest : class
{
var builder = _routeBuilder.MapGet(path, ctx => BuildQueryContext(ctx, context));
endpoint?.Invoke(builder);
ApplyAuthRolesAndPolicies(builder, auth, roles, policies);
AddEndpointDefinition<TRequest, TResult>(HttpMethods.Get, path);

return this;
}

public IEndpointsBuilder Post(string path, Func<HttpContext, Task> context = null,
Action<IEndpointConventionBuilder> endpoint = null, bool auth = false, string roles = null,
params string[] policies)
{
var builder = _routeBuilder.MapPost(path, ctx => context?.Invoke(ctx));
endpoint?.Invoke(builder);
ApplyAuthRolesAndPolicies(builder, auth, roles, policies);
AddEndpointDefinition(HttpMethods.Post, path);

return this;
}

public IEndpointsBuilder Post<T>(string path, Func<T, HttpContext, Task> context = null,
Action<IEndpointConventionBuilder> endpoint = null, bool auth = false, string roles = null,
params string[] policies) where T : class
{
var builder = _routeBuilder.MapPost(path, ctx => BuildRequestContext(ctx, context));
endpoint?.Invoke(builder);
ApplyAuthRolesAndPolicies(builder, auth, roles, policies);
AddEndpointDefinition<T>(HttpMethods.Post, path);

return this;
}

public IEndpointsBuilder Put(string path, Func<HttpContext, Task> context = null,
Action<IEndpointConventionBuilder> endpoint = null, bool auth = false, string roles = null,
params string[] policies)
{
var builder = _routeBuilder.MapPut(path, ctx => context?.Invoke(ctx));
endpoint?.Invoke(builder);
ApplyAuthRolesAndPolicies(builder, auth, roles, policies);
AddEndpointDefinition(HttpMethods.Put, path);

return this;
}

public IEndpointsBuilder Put<T>(string path, Func<T, HttpContext, Task> context = null,
Action<IEndpointConventionBuilder> endpoint = null, bool auth = false, string roles = null,
params string[] policies) where T : class
{
var builder = _routeBuilder.MapPut(path, ctx => BuildRequestContext(ctx, context));
endpoint?.Invoke(builder);
ApplyAuthRolesAndPolicies(builder, auth, roles, policies);
AddEndpointDefinition<T>(HttpMethods.Put, path);

return this;
}

public IEndpointsBuilder Delete(string path, Func<HttpContext, Task> context = null,
Action<IEndpointConventionBuilder> endpoint = null, bool auth = false, string roles = null,
params string[] policies)
{
var builder = _routeBuilder.MapDelete(path, ctx => context?.Invoke(ctx));
endpoint?.Invoke(builder);
ApplyAuthRolesAndPolicies(builder, auth, roles, policies);
AddEndpointDefinition(HttpMethods.Delete, path);

return this;
}

public IEndpointsBuilder Delete<T>(string path, Func<T, HttpContext, Task> context = null,
Action<IEndpointConventionBuilder> endpoint = null, bool auth = false, string roles = null,
params string[] policies) where T : class
{
var builder = _routeBuilder.MapDelete(path, ctx => BuildQueryContext(ctx, context));
endpoint?.Invoke(builder);
ApplyAuthRolesAndPolicies(builder, auth, roles, policies);
AddEndpointDefinition<T>(HttpMethods.Delete, path);

return this;
}

private static void ApplyAuthRolesAndPolicies(IEndpointConventionBuilder builder, bool auth, string roles,
params string[] policies)
{
if (policies is not null && policies.Any())
{
builder.RequireAuthorization(policies);
return;
}

var hasRoles = !string.IsNullOrWhiteSpace(roles);
var authorize = new AuthorizeAttribute();
if (hasRoles)
{
authorize.Roles = roles;
}

if (auth || hasRoles)
{
builder.RequireAuthorization(authorize);
}
}

private static async Task BuildRequestContext<T>(HttpContext httpContext,
Func<T, HttpContext, Task> context = null) where T : class
{
var request = await httpContext.ReadJsonAsync<T>();
if (request is null || context is null)
{
return;
}

await context.Invoke(request, httpContext);
}

private static async Task BuildQueryContext<T>(HttpContext httpContext,
Func<T, HttpContext, Task> context = null) where T : class
{
var request = httpContext.ReadQuery<T>();
if (request is null || context is null)
{
return;
}

await context.Invoke(request, httpContext);
}

private void AddEndpointDefinition(string method, string path)
{
_definitions.Add(new WebApiEndpointDefinition
{
Method = method,
Path = path,
Responses = new List<WebApiEndpointResponse>
{
new WebApiEndpointResponse { StatusCode = 200 }
}
});
}

private void AddEndpointDefinition<T>(string method, string path)
=> AddEndpointDefinition(method, path, typeof(T), null);

private void AddEndpointDefinition<TRequest, TResult>(string method, string path)
=> AddEndpointDefinition(method, path, typeof(TRequest), typeof(TResult));

private void AddEndpointDefinition(string method, string path, Type input, Type output)
{
if (_definitions.Exists(d => d.Path == path && d.Method == method))
{
return;
}

_definitions.Add(new WebApiEndpointDefinition
{
Method = method,
Path = path,
Parameters = new List<WebApiEndpointParameter>
{
new WebApiEndpointParameter
{
In = method == HttpMethods.Get ? "query" : "body",
Name = input?.Name,
Type = input,
Example = input?.GetDefaultInstance()
}
},
Responses = new List<WebApiEndpointResponse>
{
new WebApiEndpointResponse
{
StatusCode = method == HttpMethods.Get ? 200 : 202,
Type = output,
Example = output?.GetDefaultInstance()
}
}
});
}
}
}
57 changes: 57 additions & 0 deletions src/Paralax.WebApi/src/Exceptions/ErrorHandlerMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using NetJSON;

namespace Paralax.WebApi.Exceptions
{
internal sealed class ErrorHandlerMiddleware : IMiddleware
{
private readonly IExceptionToResponseMapper _exceptionToResponseMapper;
private readonly ILogger<ErrorHandlerMiddleware> _logger;

public ErrorHandlerMiddleware(IExceptionToResponseMapper exceptionToResponseMapper,
ILogger<ErrorHandlerMiddleware> logger)
{
_exceptionToResponseMapper = exceptionToResponseMapper;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while processing the request.");
await HandleErrorAsync(context, exception); // Handle the error when caught
}
}

private async Task HandleErrorAsync(HttpContext context, Exception exception)
{
var exceptionResponse = _exceptionToResponseMapper.Map(exception);

context.Response.StatusCode = (int)(exceptionResponse?.StatusCode ?? HttpStatusCode.BadRequest);

var response = exceptionResponse?.Response;

if (response is null)
{
await context.Response.WriteAsync(string.Empty);
return;
}

context.Response.ContentType = "application/json";

// Use NetJSON to serialize the response
var jsonResponse = NetJSON.NetJSON.Serialize(response);

await context.Response.WriteAsync(jsonResponse);
}
}
}
45 changes: 45 additions & 0 deletions src/Paralax.WebApi/src/Exceptions/ExceptionResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Net;

namespace Paralax.WebApi.Exceptions
{
public class ExceptionResponse
{
/// <summary>
/// The response object containing error details or messages.
/// </summary>
public object Response { get; }

/// <summary>
/// The HTTP status code associated with this exception response.
/// </summary>
public HttpStatusCode StatusCode { get; }

/// <summary>
/// Constructor for creating an instance of ExceptionResponse.
/// </summary>
/// <param name="response">The response object, typically containing error messages or details.</param>
/// <param name="statusCode">The HTTP status code indicating the type of error.</param>
public ExceptionResponse(object response, HttpStatusCode statusCode)
{
Response = response;
StatusCode = statusCode;
}

/// <summary>
/// A utility method to create a standard error response.
/// </summary>
/// <param name="message">Error message to return in the response.</param>
/// <param name="statusCode">HTTP status code of the error.</param>
/// <returns>A structured exception response with the message and status code.</returns>
public static ExceptionResponse Create(string message, HttpStatusCode statusCode)
{
var response = new
{
error = true,
message = message
};

return new ExceptionResponse(response, statusCode);
}
}
}
10 changes: 10 additions & 0 deletions src/Paralax.WebApi/src/Exceptions/IExceptionToResponseMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace Paralax.WebApi.Exceptions
{
public interface IExceptionToResponseMapper
{
ExceptionResponse Map(Exception exception);
}
}

Loading

0 comments on commit bf55b75

Please sign in to comment.