1.更换软件协议为AGPL
2.切换项目名称为Hua.Todo
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Hua.Todo.Application.DynamicApi;
|
||||
|
||||
public static class DynamicApiExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseDynamicApi(this IApplicationBuilder builder)
|
||||
{
|
||||
return builder.UseMiddleware<DynamicApiMiddleware>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,637 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Hua.Todo.Application.Interfaces;
|
||||
|
||||
namespace Hua.Todo.Application.DynamicApi;
|
||||
|
||||
public class DynamicApiMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private static readonly Dictionary<string, Type> _serviceTypeCache = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static DynamicApiMiddleware()
|
||||
{
|
||||
InitializeServiceTypeCache();
|
||||
}
|
||||
|
||||
private static void InitializeServiceTypeCache()
|
||||
{
|
||||
var assembly = typeof(IDynamicApiService).Assembly;
|
||||
var serviceTypes = assembly.GetTypes()
|
||||
.Where(t => t.IsInterface && typeof(IDynamicApiService).IsAssignableFrom(t))
|
||||
.ToList();
|
||||
|
||||
foreach (var type in serviceTypes)
|
||||
{
|
||||
var cleanName = type.Name.EndsWith("Service")
|
||||
? type.Name.Substring(0, type.Name.Length - "Service".Length)
|
||||
: type.Name;
|
||||
|
||||
if (cleanName.StartsWith("I") && cleanName.Length > 1 && char.IsUpper(cleanName[1]))
|
||||
{
|
||||
cleanName = cleanName.Substring(1);
|
||||
}
|
||||
|
||||
if (cleanName.EndsWith("App"))
|
||||
{
|
||||
cleanName = cleanName.Substring(0, cleanName.Length - "App".Length);
|
||||
}
|
||||
|
||||
_serviceTypeCache[cleanName] = type;
|
||||
}
|
||||
}
|
||||
|
||||
public DynamicApiMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
|
||||
{
|
||||
_next = next;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var path = context.Request.Path.Value ?? string.Empty;
|
||||
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (segments.Length < 2 || segments[0] != "api")
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var serviceName = segments[1];
|
||||
var serviceType = FindDynamicApiService(serviceName);
|
||||
|
||||
if (serviceType == null)
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsRemoteServiceEnabled(serviceType))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var scopedServiceProvider = scope.ServiceProvider;
|
||||
var service = scopedServiceProvider.GetService(serviceType);
|
||||
if (service == null)
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var method = FindMethod(serviceType, context.Request.Method, segments.Skip(2).ToArray(), context);
|
||||
if (method == null)
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsRemoteServiceEnabled(method))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await InvokeMethod(service, method, context);
|
||||
await WriteResponse(context, result, null, method.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await WriteResponse(context, null, ex, method.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private Type? FindDynamicApiService(string serviceName)
|
||||
{
|
||||
_serviceTypeCache.TryGetValue(serviceName, out var serviceType);
|
||||
return serviceType;
|
||||
}
|
||||
|
||||
private bool IsRemoteServiceEnabled(Type type)
|
||||
{
|
||||
var attribute = type.GetCustomAttribute<RemoteServiceAttribute>();
|
||||
return attribute == null || attribute.IsEnabled;
|
||||
}
|
||||
|
||||
private bool IsRemoteServiceEnabled(MethodInfo method)
|
||||
{
|
||||
var attribute = method.GetCustomAttribute<RemoteServiceAttribute>();
|
||||
return attribute == null || attribute.IsEnabled;
|
||||
}
|
||||
|
||||
private MethodInfo? FindMethod(Type serviceType, string httpMethod, string[] pathSegments, HttpContext context)
|
||||
{
|
||||
var methods = serviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
var matchedMethods = methods.Where(m => IsRemoteServiceEnabled(m)).ToList();
|
||||
|
||||
// First, try to handle special case: /api/task/13/toggle
|
||||
if (pathSegments.Length >= 2 && httpMethod == "PATCH")
|
||||
{
|
||||
var potentialMethodName = pathSegments[1];
|
||||
var potentialParamValue = pathSegments[0];
|
||||
|
||||
// Try to find method with matching name and single parameter
|
||||
foreach (var method in matchedMethods)
|
||||
{
|
||||
if (MatchesHttpMethod(method, httpMethod))
|
||||
{
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length == 1 && IsSimpleType(parameters[0].ParameterType))
|
||||
{
|
||||
// Try to match with normalized method name
|
||||
var normalizedMethodName = method.Name;
|
||||
if (normalizedMethodName.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalizedMethodName = normalizedMethodName.Substring(0, normalizedMethodName.Length - 5);
|
||||
}
|
||||
|
||||
if (normalizedMethodName.Equals(potentialMethodName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return method;
|
||||
}
|
||||
|
||||
// For ToggleCompleteAsync, try to match with "toggle"
|
||||
if (normalizedMethodName.StartsWith("Toggle", StringComparison.OrdinalIgnoreCase) &&
|
||||
potentialMethodName.Equals("toggle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to match with method name from path segments
|
||||
var methodName = pathSegments.Length > 0 ? pathSegments[0] : null;
|
||||
if (!string.IsNullOrEmpty(methodName))
|
||||
{
|
||||
var exactMatch = matchedMethods.FirstOrDefault(m =>
|
||||
m.Name.Equals(methodName, StringComparison.OrdinalIgnoreCase) &&
|
||||
MatchesHttpMethod(m, httpMethod));
|
||||
|
||||
if (exactMatch != null)
|
||||
return exactMatch;
|
||||
|
||||
// Try to match method names with different naming conventions
|
||||
foreach (var method in matchedMethods)
|
||||
{
|
||||
if (MatchesHttpMethod(method, httpMethod))
|
||||
{
|
||||
// For GetSubTasksAsync, try to match with "subtasks"
|
||||
var normalizedMethodName = method.Name;
|
||||
if (normalizedMethodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalizedMethodName = normalizedMethodName.Substring(3);
|
||||
}
|
||||
if (normalizedMethodName.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalizedMethodName = normalizedMethodName.Substring(0, normalizedMethodName.Length - 5);
|
||||
}
|
||||
|
||||
if (normalizedMethodName.Equals(methodName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return method;
|
||||
}
|
||||
|
||||
// For GetActiveTasksAsync, try to match with "active"
|
||||
if (normalizedMethodName.Equals($"{methodName}Tasks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First, try to handle special case: /api/task/13/subtasks
|
||||
if (pathSegments.Length >= 2)
|
||||
{
|
||||
var potentialMethodName = pathSegments[1];
|
||||
var potentialParamValue = pathSegments[0];
|
||||
|
||||
// Try to find method with matching name and single parameter
|
||||
foreach (var method in matchedMethods)
|
||||
{
|
||||
if (MatchesHttpMethod(method, httpMethod))
|
||||
{
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length == 1 && IsSimpleType(parameters[0].ParameterType))
|
||||
{
|
||||
// Try to match with normalized method name
|
||||
var normalizedMethodName = method.Name;
|
||||
if (normalizedMethodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalizedMethodName = normalizedMethodName.Substring(3);
|
||||
}
|
||||
if (normalizedMethodName.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalizedMethodName = normalizedMethodName.Substring(0, normalizedMethodName.Length - 5);
|
||||
}
|
||||
|
||||
if (normalizedMethodName.Equals(potentialMethodName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var method in matchedMethods)
|
||||
{
|
||||
if (MatchesHttpMethod(method, httpMethod) && MatchesRoute(method, pathSegments))
|
||||
{
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to match methods with parameters from path
|
||||
foreach (var method in matchedMethods)
|
||||
{
|
||||
if (MatchesHttpMethod(method, httpMethod))
|
||||
{
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length > 0 && pathSegments.Length > 0)
|
||||
{
|
||||
// Check if all path segments can be mapped to parameters
|
||||
bool canMapParameters = true;
|
||||
for (int i = 0; i < pathSegments.Length; i++)
|
||||
{
|
||||
if (i >= parameters.Length)
|
||||
{
|
||||
canMapParameters = false;
|
||||
break;
|
||||
}
|
||||
if (!IsSimpleType(parameters[i].ParameterType))
|
||||
{
|
||||
canMapParameters = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (canMapParameters)
|
||||
{
|
||||
// Try to convert the first path segment to the parameter type
|
||||
// to avoid trying to convert non-numeric strings to numbers
|
||||
try
|
||||
{
|
||||
ConvertValue(pathSegments[0], parameters[0].ParameterType);
|
||||
return method;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If conversion fails, skip this method
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool MatchesHttpMethod(MethodInfo method, string httpMethod)
|
||||
{
|
||||
if (method.GetCustomAttribute<HttpGetAttribute>() != null)
|
||||
return httpMethod == "GET";
|
||||
|
||||
if (method.GetCustomAttribute<HttpPostAttribute>() != null)
|
||||
return httpMethod == "POST";
|
||||
|
||||
if (method.GetCustomAttribute<HttpPutAttribute>() != null)
|
||||
return httpMethod == "PUT";
|
||||
|
||||
if (method.GetCustomAttribute<HttpDeleteAttribute>() != null)
|
||||
return httpMethod == "DELETE";
|
||||
|
||||
if (method.GetCustomAttribute<HttpPatchAttribute>() != null)
|
||||
return httpMethod == "PATCH";
|
||||
|
||||
// For ToggleCompleteAsync, use PATCH method
|
||||
if (method.Name.StartsWith("Toggle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return httpMethod == "PATCH";
|
||||
}
|
||||
|
||||
return GetHttpVerbByConvention(method.Name) == httpMethod;
|
||||
}
|
||||
|
||||
private string GetHttpVerbByConvention(string methodName)
|
||||
{
|
||||
if (methodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase))
|
||||
return "GET";
|
||||
|
||||
if (methodName.StartsWith("Put", StringComparison.OrdinalIgnoreCase) ||
|
||||
methodName.StartsWith("Update", StringComparison.OrdinalIgnoreCase))
|
||||
return "PUT";
|
||||
|
||||
if (methodName.StartsWith("Delete", StringComparison.OrdinalIgnoreCase) ||
|
||||
methodName.StartsWith("Remove", StringComparison.OrdinalIgnoreCase))
|
||||
return "DELETE";
|
||||
|
||||
if (methodName.StartsWith("Post", StringComparison.OrdinalIgnoreCase) ||
|
||||
methodName.StartsWith("Create", StringComparison.OrdinalIgnoreCase) ||
|
||||
methodName.StartsWith("Insert", StringComparison.OrdinalIgnoreCase))
|
||||
return "POST";
|
||||
|
||||
if (methodName.StartsWith("Patch", StringComparison.OrdinalIgnoreCase))
|
||||
return "PATCH";
|
||||
|
||||
return "POST";
|
||||
}
|
||||
|
||||
private bool MatchesRoute(MethodInfo method, string[] pathSegments)
|
||||
{
|
||||
var httpGetAttr = method.GetCustomAttribute<HttpGetAttribute>();
|
||||
var httpPostAttr = method.GetCustomAttribute<HttpPostAttribute>();
|
||||
var httpPutAttr = method.GetCustomAttribute<HttpPutAttribute>();
|
||||
var httpDeleteAttr = method.GetCustomAttribute<HttpDeleteAttribute>();
|
||||
var httpPatchAttr = method.GetCustomAttribute<HttpPatchAttribute>();
|
||||
|
||||
var route = httpGetAttr?.Route ?? httpPostAttr?.Route ??
|
||||
httpPutAttr?.Route ?? httpDeleteAttr?.Route ?? httpPatchAttr?.Route;
|
||||
|
||||
if (!string.IsNullOrEmpty(route))
|
||||
{
|
||||
var routeSegments = route.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (routeSegments.Length > 0 && pathSegments.Length > 0)
|
||||
{
|
||||
return routeSegments[0].Equals(pathSegments[0], StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
return routeSegments.Length == 0 && pathSegments.Length == 0;
|
||||
}
|
||||
|
||||
if (pathSegments.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to match with full method name
|
||||
if (method.Name.Equals(pathSegments[0], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to match with normalized method name (without Get/Async prefix/suffix)
|
||||
var normalizedMethodName = method.Name;
|
||||
if (normalizedMethodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalizedMethodName = normalizedMethodName.Substring(3);
|
||||
}
|
||||
if (normalizedMethodName.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalizedMethodName = normalizedMethodName.Substring(0, normalizedMethodName.Length - 5);
|
||||
}
|
||||
|
||||
return normalizedMethodName.Equals(pathSegments[0], StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task<object?> InvokeMethod(object service, MethodInfo method, HttpContext context)
|
||||
{
|
||||
var parameters = method.GetParameters();
|
||||
var args = new List<object?>();
|
||||
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
var fromBodyAttr = param.GetCustomAttribute<FromBodyAttribute>();
|
||||
var fromQueryAttr = param.GetCustomAttribute<FromQueryAttribute>();
|
||||
|
||||
if (fromBodyAttr != null)
|
||||
{
|
||||
var dto = await ReadDtoFromBody(context, param.ParameterType);
|
||||
args.Add(dto);
|
||||
}
|
||||
else if (fromQueryAttr != null || IsSimpleType(param.ParameterType))
|
||||
{
|
||||
var value = BindParameterFromQueryOrPath(param, context);
|
||||
args.Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dto = await ReadDtoFromBody(context, param.ParameterType);
|
||||
args.Add(dto);
|
||||
}
|
||||
}
|
||||
|
||||
var result = method.Invoke(service, args.ToArray());
|
||||
|
||||
if (result is not Task task) return result;
|
||||
await task;
|
||||
return task.GetType().GetProperty("Result")?.GetValue(task);
|
||||
|
||||
}
|
||||
|
||||
private bool IsSimpleType(Type type)
|
||||
{
|
||||
return type.IsPrimitive ||
|
||||
type == typeof(string) ||
|
||||
type == typeof(decimal) ||
|
||||
type == typeof(DateTime) ||
|
||||
type == typeof(Guid) ||
|
||||
Nullable.GetUnderlyingType(type) != null;
|
||||
}
|
||||
|
||||
private object? BindParameterFromQueryOrPath(ParameterInfo param, HttpContext context)
|
||||
{
|
||||
var paramName = param.Name ?? string.Empty;
|
||||
|
||||
// Try to get value from path
|
||||
var pathValue = GetPathValue(context, paramName);
|
||||
if (!string.IsNullOrEmpty(pathValue))
|
||||
{
|
||||
return ConvertValue(pathValue, param.ParameterType);
|
||||
}
|
||||
|
||||
// Try to get value from query string
|
||||
var queryValue = context.Request.Query[paramName].FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(queryValue))
|
||||
{
|
||||
return ConvertValue(queryValue, param.ParameterType);
|
||||
}
|
||||
|
||||
// For methods with single parameter, try to get value from path segments
|
||||
var segments = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (segments != null && segments.Length >= 3)
|
||||
{
|
||||
// For GET /api/task/13, segments = ["api", "task", "13"]
|
||||
// For GET /api/task/13/subtasks, segments = ["api", "task", "13", "subtasks"]
|
||||
var methodName = segments.Length > 3 ? segments[3] : null;
|
||||
var paramValue = segments[2];
|
||||
|
||||
// Check if this is a method with single parameter
|
||||
var method = param.Member as MethodInfo;
|
||||
if (method != null && method.GetParameters().Length == 1)
|
||||
{
|
||||
return ConvertValue(paramValue, param.ParameterType);
|
||||
}
|
||||
|
||||
// Special case for GetSubTasksAsync
|
||||
if (methodName?.Equals("subtasks", StringComparison.OrdinalIgnoreCase) == true &&
|
||||
paramName.Equals("parentTaskId", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ConvertValue(paramValue, param.ParameterType);
|
||||
}
|
||||
}
|
||||
|
||||
if (param.ParameterType.IsValueType && Nullable.GetUnderlyingType(param.ParameterType) == null)
|
||||
{
|
||||
throw new ArgumentException($"Parameter '{paramName}' is required");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private object? ConvertValue(string value, Type targetType)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (targetType == typeof(string))
|
||||
return value;
|
||||
|
||||
if (targetType == typeof(int) || targetType == typeof(int?))
|
||||
return int.Parse(value);
|
||||
|
||||
if (targetType == typeof(long) || targetType == typeof(long?))
|
||||
return long.Parse(value);
|
||||
|
||||
if (targetType == typeof(bool) || targetType == typeof(bool?))
|
||||
return bool.Parse(value);
|
||||
|
||||
if (targetType == typeof(decimal) || targetType == typeof(decimal?))
|
||||
return decimal.Parse(value);
|
||||
|
||||
if (targetType == typeof(DateTime) || targetType == typeof(DateTime?))
|
||||
return DateTime.Parse(value);
|
||||
|
||||
if (targetType == typeof(Guid) || targetType == typeof(Guid?))
|
||||
return Guid.Parse(value);
|
||||
|
||||
return Convert.ChangeType(value, targetType);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new ArgumentException($"Cannot convert '{value}' to {targetType.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetPathValue(HttpContext context, string? paramName)
|
||||
{
|
||||
var segments = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (segments == null || segments.Length < 3)
|
||||
return null;
|
||||
|
||||
// Try to find parameter in path segments
|
||||
// For GET /api/task/13, segments = ["api", "task", "13"]
|
||||
// For GET /api/task/13/subtasks, segments = ["api", "task", "13", "subtasks"]
|
||||
if (segments.Length >= 3)
|
||||
{
|
||||
// First check if paramName is "id" (common case)
|
||||
if (paramName?.Equals("id", StringComparison.OrdinalIgnoreCase) == true)
|
||||
return segments[2];
|
||||
|
||||
// Then check if paramName is "parentTaskId" (for subtasks)
|
||||
if (paramName?.Equals("parentTaskId", StringComparison.OrdinalIgnoreCase) == true && segments.Length >= 3)
|
||||
return segments[2];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<object?> ReadDtoFromBody(HttpContext context, Type dtoType)
|
||||
{
|
||||
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
return null;
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = {
|
||||
new System.Text.Json.Serialization.JsonStringEnumConverter()
|
||||
}
|
||||
};
|
||||
|
||||
return JsonSerializer.Deserialize(body, dtoType, options);
|
||||
}
|
||||
|
||||
private async Task WriteResponse(HttpContext context, object? result, Exception? exception, string methodName)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
var message = methodName switch
|
||||
{
|
||||
_ when methodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase) => "获取成功",
|
||||
_ when methodName.StartsWith("Create", StringComparison.OrdinalIgnoreCase) => "创建成功",
|
||||
_ when methodName.StartsWith("Update", StringComparison.OrdinalIgnoreCase) => "更新成功",
|
||||
_ when methodName.StartsWith("Delete", StringComparison.OrdinalIgnoreCase) => "删除成功",
|
||||
_ when methodName.StartsWith("Toggle", StringComparison.OrdinalIgnoreCase) => "操作成功",
|
||||
_ => "操作成功"
|
||||
};
|
||||
|
||||
var errorMessage = methodName switch
|
||||
{
|
||||
_ when methodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase) => "获取失败",
|
||||
_ when methodName.StartsWith("Create", StringComparison.OrdinalIgnoreCase) => "创建失败",
|
||||
_ when methodName.StartsWith("Update", StringComparison.OrdinalIgnoreCase) => "更新失败",
|
||||
_ when methodName.StartsWith("Delete", StringComparison.OrdinalIgnoreCase) => "删除失败",
|
||||
_ when methodName.StartsWith("Toggle", StringComparison.OrdinalIgnoreCase) => "操作失败",
|
||||
_ => "操作失败"
|
||||
};
|
||||
|
||||
// Get friendly error message
|
||||
var errors = new List<string>();
|
||||
if (exception != null)
|
||||
{
|
||||
if (exception is Microsoft.EntityFrameworkCore.DbUpdateException dbEx)
|
||||
{
|
||||
var innerMessage = dbEx.InnerException?.Message ?? dbEx.Message;
|
||||
if (innerMessage.Contains("no such table", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
errors.Add("数据库表不存在,请尝试重启应用或重新初始化数据库。");
|
||||
}
|
||||
else if (innerMessage.Contains("UNIQUE constraint failed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
errors.Add("该项已存在,请检查是否重复。");
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add($"数据库操作失败: {innerMessage}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
var response = new
|
||||
{
|
||||
Success = exception == null,
|
||||
Data = result,
|
||||
Message = exception == null ? message : errorMessage,
|
||||
Errors = errors.Count > 0 ? errors : null
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(response, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
context.Response.StatusCode = exception switch
|
||||
{
|
||||
KeyNotFoundException => 404,
|
||||
ArgumentException => 400,
|
||||
_ => 200
|
||||
};
|
||||
|
||||
await context.Response.WriteAsync(json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
namespace Hua.Todo.Application.DynamicApi;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class HttpGetAttribute : Attribute
|
||||
{
|
||||
public string? Route { get; set; }
|
||||
|
||||
public HttpGetAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
public HttpGetAttribute(string route)
|
||||
{
|
||||
Route = route;
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class HttpPostAttribute : Attribute
|
||||
{
|
||||
public string? Route { get; set; }
|
||||
|
||||
public HttpPostAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
public HttpPostAttribute(string route)
|
||||
{
|
||||
Route = route;
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class HttpPutAttribute : Attribute
|
||||
{
|
||||
public string? Route { get; set; }
|
||||
|
||||
public HttpPutAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
public HttpPutAttribute(string route)
|
||||
{
|
||||
Route = route;
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class HttpDeleteAttribute : Attribute
|
||||
{
|
||||
public string? Route { get; set; }
|
||||
|
||||
public HttpDeleteAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
public HttpDeleteAttribute(string route)
|
||||
{
|
||||
Route = route;
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class HttpPatchAttribute : Attribute
|
||||
{
|
||||
public string? Route { get; set; }
|
||||
|
||||
public HttpPatchAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
public HttpPatchAttribute(string route)
|
||||
{
|
||||
Route = route;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Hua.Todo.Application.DynamicApi;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
|
||||
public class FromQueryAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
|
||||
public class FromBodyAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Hua.Todo.Application.DynamicApi;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class RemoteServiceAttribute : Attribute
|
||||
{
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public bool IsMetadataEnabled { get; set; } = true;
|
||||
}
|
||||
Reference in New Issue
Block a user