sync full source from private myoffice
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
namespace MyOffice.Web.Infrastructure.Attributes
|
||||
{
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Core.Extensions;
|
||||
|
||||
public class AsGuidAttribute: ValidationAttribute
|
||||
{
|
||||
private readonly bool _nullable;
|
||||
|
||||
public AsGuidAttribute(bool nullable = false)
|
||||
{
|
||||
_nullable = nullable;
|
||||
}
|
||||
|
||||
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
|
||||
{
|
||||
if (value == null && _nullable)
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
if (value == null || value.ToString().IsMissing())
|
||||
{
|
||||
return new ValidationResult("Id parameter is not valid");
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(value.ToString(), out _))
|
||||
{
|
||||
return new ValidationResult("Id parameter is not valid");
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
namespace MyOffice.Web.Infrastructure.Attributes;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class DefaultFromBodyAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
public class DefaultFromBodyBindingConvention : IActionModelConvention
|
||||
{
|
||||
public void Apply(ActionModel action)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
if (action.Controller.Attributes.Any(x => x is DefaultFromBodyAttribute))
|
||||
{
|
||||
foreach (var parameter in action.Parameters)
|
||||
{
|
||||
if (parameter.Attributes.Any(x =>
|
||||
x is FromQueryAttribute
|
||||
or FromRouteAttribute
|
||||
or FromHeaderAttribute
|
||||
or FromFormAttribute
|
||||
or FromServicesAttribute))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Already bound (e.g. CancellationToken → Special) — do not force Body.
|
||||
if (parameter.BindingInfo?.BindingSource is { } existing
|
||||
&& existing != BindingSource.ModelBinding
|
||||
&& existing != BindingSource.Custom)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var paramType = parameter.ParameterInfo.ParameterType;
|
||||
if (paramType == typeof(CancellationToken)
|
||||
|| Nullable.GetUnderlyingType(paramType) == typeof(CancellationToken))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var isSimpleType = paramType.IsPrimitive
|
||||
|| paramType.IsEnum
|
||||
|| paramType == typeof(string)
|
||||
|| paramType == typeof(decimal)
|
||||
|| paramType == typeof(Guid)
|
||||
|| paramType == typeof(Guid?)
|
||||
|| paramType == typeof(DateTime)
|
||||
|| paramType == typeof(DateTime?)
|
||||
|| paramType == typeof(DateOnly)
|
||||
|| paramType == typeof(DateOnly?)
|
||||
|| paramType == typeof(TimeOnly)
|
||||
|| paramType == typeof(TimeOnly?);
|
||||
|
||||
if (!isSimpleType)
|
||||
{
|
||||
parameter.BindingInfo ??= new BindingInfo();
|
||||
parameter.BindingInfo.BindingSource = BindingSource.Body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace MyOffice.Web.Infrastructure.Attributes;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
||||
public class GlobalModelStateValidatorAttribute : ActionFilterAttribute
|
||||
{
|
||||
private readonly ProblemDetailsFactory _problemDetailsFactory;
|
||||
|
||||
public GlobalModelStateValidatorAttribute(ProblemDetailsFactory problemDetailsFactory)
|
||||
{
|
||||
_problemDetailsFactory = problemDetailsFactory;
|
||||
}
|
||||
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (!context.ModelState.IsValid)
|
||||
{
|
||||
context.Result = new ObjectResult(_problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState))
|
||||
{
|
||||
StatusCode = StatusCodes.Status400BadRequest
|
||||
};
|
||||
}
|
||||
|
||||
base.OnActionExecuting(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
namespace MyOffice.Web.Infrastructure;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
public class CustomProblemDetailsFactory : ProblemDetailsFactory
|
||||
{
|
||||
private readonly ApiBehaviorOptions options;
|
||||
private readonly JsonOptions jsonOptions;
|
||||
|
||||
public CustomProblemDetailsFactory(IOptions<ApiBehaviorOptions> options, IOptions<JsonOptions> jsonOptions)
|
||||
{
|
||||
this.options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
this.jsonOptions = jsonOptions?.Value ?? throw new ArgumentNullException(nameof(jsonOptions));
|
||||
}
|
||||
|
||||
public override ProblemDetails CreateProblemDetails(
|
||||
HttpContext httpContext,
|
||||
int? statusCode = null,
|
||||
string? title = null,
|
||||
string? type = null,
|
||||
string? detail = null,
|
||||
string? instance = null)
|
||||
{
|
||||
statusCode ??= 500;
|
||||
|
||||
var problemDetails = new ProblemDetails
|
||||
{
|
||||
Status = statusCode,
|
||||
Title = title,
|
||||
Type = type,
|
||||
Detail = detail,
|
||||
Instance = instance,
|
||||
};
|
||||
|
||||
ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);
|
||||
|
||||
return problemDetails;
|
||||
}
|
||||
|
||||
public override ValidationProblemDetails CreateValidationProblemDetails(
|
||||
HttpContext httpContext,
|
||||
ModelStateDictionary modelStateDictionary,
|
||||
int? statusCode = null,
|
||||
string? title = null,
|
||||
string? type = null,
|
||||
string? detail = null,
|
||||
string? instance = null)
|
||||
{
|
||||
if (modelStateDictionary == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelStateDictionary));
|
||||
}
|
||||
|
||||
statusCode ??= 400;
|
||||
|
||||
var errors = modelStateDictionary
|
||||
.Where(x => x.Value?.Errors.Any() == true)
|
||||
.ToDictionary(
|
||||
kvp => jsonOptions?.JsonSerializerOptions?.PropertyNamingPolicy?.ConvertName(kvp.Key) ?? kvp.Key,
|
||||
kvp => kvp.Value!.Errors.Select(x => x.ErrorMessage).ToArray()
|
||||
);
|
||||
|
||||
var problemDetails = new ValidationProblemDetails(errors)
|
||||
{
|
||||
Status = statusCode,
|
||||
Type = type,
|
||||
Detail = detail,
|
||||
Instance = instance,
|
||||
};
|
||||
|
||||
if (title != null)
|
||||
{
|
||||
// For validation problem details, don't overwrite the default title with null.
|
||||
problemDetails.Title = title;
|
||||
}
|
||||
|
||||
ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);
|
||||
|
||||
return problemDetails;
|
||||
}
|
||||
|
||||
private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode)
|
||||
{
|
||||
problemDetails.Status ??= statusCode;
|
||||
|
||||
if (options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData))
|
||||
{
|
||||
problemDetails.Title ??= clientErrorData.Title;
|
||||
problemDetails.Type ??= clientErrorData.Link;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace MyOffice.Web.Infrastructure;
|
||||
|
||||
using DbContext;
|
||||
|
||||
/// <summary>
|
||||
/// Runs EF migrate + seed on startup with a scoped DbContext.
|
||||
/// </summary>
|
||||
public sealed class DatabaseInitializerHostedService : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<DatabaseInitializerHostedService> _logger;
|
||||
|
||||
public DatabaseInitializerHostedService(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<DatabaseInitializerHostedService> logger
|
||||
)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = _serviceProvider.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
_logger.LogInformation("Applying database migrations and seed data…");
|
||||
await DatabaseBootstrapper.InitializeAsync(db, cancellationToken);
|
||||
_logger.LogInformation("Database ready.");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace MyOffice.Web.Infrastructure.Filters;
|
||||
|
||||
using System.Collections;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using MyOffice.Web.Models;
|
||||
|
||||
public class ResponseFilter : IActionFilter
|
||||
{
|
||||
private readonly ILogger<ResponseFilter> _logger;
|
||||
|
||||
public ResponseFilter(ILogger<ResponseFilter> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
// TODO: restore
|
||||
/*var route = $"{context.Controller.GetType().Name}.{context.ActionDescriptor.DisplayName}";
|
||||
if (context.Result == null)
|
||||
throw new NotSupportedException($"[{route}] Response required");
|
||||
if (!(context.Result is ObjectResult result))
|
||||
throw new NotSupportedException($"[{route}] Response must be an ObjectResult - {context.Result?.GetType().Name}");
|
||||
if (result.Value == null)
|
||||
throw new NotSupportedException($"[{route}] Response must be an ObjectResult with value");
|
||||
|
||||
if (result.Value is IResponseModel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var type = result.Value.GetType();
|
||||
if (type.IsGenericType
|
||||
&& result.Value is IEnumerable
|
||||
&& type.GenericTypeArguments.Any(x => x.GetInterfaces().Any(y => y == typeof(IResponseModel))))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"[{route}] Response must be an ObjectResult with an IResponseModel - {result.Value?.GetType().Name}");*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MyOffice.Web.Infrastructure;
|
||||
|
||||
public class GlobalSettings
|
||||
{
|
||||
public string? Host { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace MyOffice.Web.Infrastructure;
|
||||
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
public static class RouteHelperExtensions
|
||||
{
|
||||
public static void Bind<T>(this IRouteBuilder routeBuilder, Expression<Func<T, ObjectResult>> expression)
|
||||
{
|
||||
System.Console.WriteLine("Bind");
|
||||
System.Console.WriteLine(expression.Name);
|
||||
System.Console.WriteLine(expression.Body.ToString());
|
||||
routeBuilder.MapRoute("SettingsAccountAccountsGet", "api/settings/accounts", new { controller = "SettingsAccount", action = "AccountsGet" });
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomRouter : IRouter
|
||||
{
|
||||
private readonly IRouter _defaultRouter;
|
||||
private readonly string _controller;
|
||||
private readonly string _action;
|
||||
|
||||
public CustomRouter(IRouter defaultRouter, string controller, string action)
|
||||
{
|
||||
_defaultRouter = defaultRouter;
|
||||
_controller = controller;
|
||||
_action = action;
|
||||
}
|
||||
|
||||
public VirtualPathData? GetVirtualPath(VirtualPathContext context)
|
||||
{
|
||||
Console.WriteLine($"1:{context.RouteName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task RouteAsync(RouteContext context)
|
||||
{
|
||||
var headers = context.HttpContext.Request.Headers;
|
||||
var path = context.HttpContext.Request.Path.Value!.Split('/');
|
||||
|
||||
Console.WriteLine($"CustomRouter:{context.HttpContext.Request.Path.Value}");
|
||||
|
||||
context.RouteData.Values["controller"] = _controller;
|
||||
context.RouteData.Values["action"] = _action;
|
||||
|
||||
await _defaultRouter.RouteAsync(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace MyOffice.Web.Infrastructure;
|
||||
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Returns ProblemDetails for unhandled exceptions (replaces missing /Error page).
|
||||
/// </summary>
|
||||
public sealed class UnhandledExceptionHandler : IExceptionHandler
|
||||
{
|
||||
private readonly IHostEnvironment _environment;
|
||||
private readonly ILogger<UnhandledExceptionHandler> _logger;
|
||||
private readonly ProblemDetailsFactory _problemDetailsFactory;
|
||||
|
||||
public UnhandledExceptionHandler(
|
||||
IHostEnvironment environment,
|
||||
ILogger<UnhandledExceptionHandler> logger,
|
||||
ProblemDetailsFactory problemDetailsFactory
|
||||
)
|
||||
{
|
||||
_environment = environment;
|
||||
_logger = logger;
|
||||
_problemDetailsFactory = problemDetailsFactory;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> TryHandleAsync(
|
||||
HttpContext httpContext,
|
||||
Exception exception,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
_logger.LogError(exception, "Unhandled exception for {Method} {Path}",
|
||||
httpContext.Request.Method,
|
||||
httpContext.Request.Path);
|
||||
|
||||
var problem = _problemDetailsFactory.CreateProblemDetails(
|
||||
httpContext,
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "An unexpected error occurred.",
|
||||
detail: _environment.IsDevelopment() ? exception.Message : null);
|
||||
|
||||
httpContext.Response.StatusCode = problem.Status ?? StatusCodes.Status500InternalServerError;
|
||||
httpContext.Response.ContentType = "application/problem+json";
|
||||
await httpContext.Response.WriteAsJsonAsync(problem, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user