1.更换软件协议为AGPL
2.切换项目名称为Hua.Todo
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Hua.Todo.Core.Entities;
|
||||
|
||||
namespace Hua.Todo.Application.Data;
|
||||
|
||||
public class TodoDbContext : DbContext
|
||||
{
|
||||
public TodoDbContext(DbContextOptions<TodoDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<TaskEntity> Tasks { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<TaskEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("Tasks");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Title).IsRequired().HasMaxLength(200);
|
||||
entity.Property(e => e.Priority).HasDefaultValue(TaskPriority.Medium);
|
||||
entity.Property(e => e.IsCompleted).HasDefaultValue(false);
|
||||
entity.Property(e => e.CreatedAt).HasDefaultValueSql("datetime('now')");
|
||||
entity.Property(e => e.UpdatedAt).HasDefaultValueSql("datetime('now')");
|
||||
|
||||
entity.HasOne(e => e.ParentTask)
|
||||
.WithMany(e => e.SubTasks)
|
||||
.HasForeignKey(e => e.ParentTaskId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0;net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net10.0'">
|
||||
<Compile Remove="DynamicApi\\**\\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Hua.Todo.Core\Hua.Todo.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Hua.Todo.Application.Interfaces;
|
||||
|
||||
public interface IDynamicApiService
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Hua.Todo.Application.Models;
|
||||
|
||||
namespace Hua.Todo.Application.Interfaces;
|
||||
|
||||
public interface ITaskService : IDynamicApiService
|
||||
{
|
||||
Task<List<TaskDto>> GetAllTasksAsync();
|
||||
Task<TaskDto?> GetTaskByIdAsync(int id);
|
||||
Task<List<TaskDto>> GetActiveTasksAsync();
|
||||
Task<List<TaskDto>> GetCompletedTasksAsync();
|
||||
Task<TaskDto> CreateTaskAsync(CreateTaskDto dto);
|
||||
Task<TaskDto> UpdateTaskAsync(UpdateTaskDto dto);
|
||||
Task<TaskDto> ToggleCompleteAsync(int id);
|
||||
Task DeleteTaskAsync(int id);
|
||||
Task<List<TaskDto>> GetSubTasksAsync(int parentTaskId);
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Hua.Todo.Application.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hua.Todo.Application.Migrations
|
||||
{
|
||||
[DbContext(typeof(TodoDbContext))]
|
||||
[Migration("20260313044926_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1);
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Tasks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hua.Todo.Application.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Tasks",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Title = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
Priority = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 1),
|
||||
IsCompleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Tasks", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Hua.Todo.Application.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hua.Todo.Application.Migrations
|
||||
{
|
||||
[DbContext(typeof(TodoDbContext))]
|
||||
[Migration("20260313092658_AddParentTaskId")]
|
||||
partial class AddParentTaskId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<int?>("ParentTaskId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1);
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentTaskId");
|
||||
|
||||
b.ToTable("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "ParentTask")
|
||||
.WithMany("SubTasks")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("ParentTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("SubTasks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hua.Todo.Application.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddParentTaskId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ParentTaskId",
|
||||
table: "Tasks",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tasks_ParentTaskId",
|
||||
table: "Tasks",
|
||||
column: "ParentTaskId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Tasks_Tasks_ParentTaskId",
|
||||
table: "Tasks",
|
||||
column: "ParentTaskId",
|
||||
principalTable: "Tasks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Tasks_Tasks_ParentTaskId",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Tasks_ParentTaskId",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ParentTaskId",
|
||||
table: "Tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Hua.Todo.Application.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hua.Todo.Application.Migrations
|
||||
{
|
||||
[DbContext(typeof(TodoDbContext))]
|
||||
partial class TodoDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<int?>("ParentTaskId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1);
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentTaskId");
|
||||
|
||||
b.ToTable("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "ParentTask")
|
||||
.WithMany("SubTasks")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("ParentTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("SubTasks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Hua.Todo.Core.Entities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Hua.Todo.Application.Models;
|
||||
|
||||
public class CreateTaskDto
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
|
||||
|
||||
public int? ParentTaskId { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateTaskDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string? Title { get; set; }
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public TaskPriority? Priority { get; set; }
|
||||
}
|
||||
|
||||
public class TaskDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public TaskPriority Priority { get; set; }
|
||||
|
||||
public bool IsCompleted { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public int? ParentTaskId { get; set; }
|
||||
public List<TaskDto> SubTasks { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiResponse<T>
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public T? Data { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public List<string> Errors { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Hua.Todo.Application": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:53852;http://localhost:53853"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Hua.Todo.Application.Data;
|
||||
using Hua.Todo.Core.Entities;
|
||||
using Hua.Todo.Core.Interfaces;
|
||||
|
||||
namespace Hua.Todo.Application.Repositories;
|
||||
|
||||
public class TaskRepository : ITaskRepository
|
||||
{
|
||||
private readonly TodoDbContext _context;
|
||||
|
||||
public TaskRepository(TodoDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<TaskEntity>> GetAllAsync()
|
||||
{
|
||||
return await _context.Tasks
|
||||
.Include(t => t.SubTasks)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<TaskEntity?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.Tasks
|
||||
.Include(t => t.SubTasks)
|
||||
.FirstOrDefaultAsync(t => t.Id == id);
|
||||
}
|
||||
|
||||
public async Task<List<TaskEntity>> GetActiveTasksAsync()
|
||||
{
|
||||
return await _context.Tasks
|
||||
.Where(t => !t.IsCompleted)
|
||||
.OrderByDescending(t => t.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<TaskEntity>> GetCompletedTasksAsync()
|
||||
{
|
||||
return await _context.Tasks
|
||||
.Where(t => t.IsCompleted)
|
||||
.OrderByDescending(t => t.UpdatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<TaskEntity> AddAsync(TaskEntity taskEntity)
|
||||
{
|
||||
_context.Tasks.Add(taskEntity);
|
||||
await _context.SaveChangesAsync();
|
||||
return taskEntity;
|
||||
}
|
||||
|
||||
public async Task<TaskEntity> UpdateAsync(TaskEntity taskEntity)
|
||||
{
|
||||
taskEntity.UpdatedAt = DateTime.UtcNow;
|
||||
_context.Tasks.Update(taskEntity);
|
||||
await _context.SaveChangesAsync();
|
||||
return taskEntity;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var task = await _context.Tasks.FindAsync(id);
|
||||
if (task != null)
|
||||
{
|
||||
_context.Tasks.Remove(task);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TaskEntity>> GetSubTasksAsync(int parentTaskId)
|
||||
{
|
||||
return await _context.Tasks
|
||||
.Where(t => t.ParentTaskId == parentTaskId)
|
||||
.OrderByDescending(t => t.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Hua.Todo.Application.Data;
|
||||
using Hua.Todo.Application.Interfaces;
|
||||
using Hua.Todo.Application.Repositories;
|
||||
using Hua.Todo.Application.Services;
|
||||
using Hua.Todo.Core.Interfaces;
|
||||
using ITaskService = Hua.Todo.Application.Interfaces.ITaskService;
|
||||
|
||||
namespace Hua.Todo.Application;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services, string connectionString)
|
||||
{
|
||||
services.AddDbContext<TodoDbContext>(options =>
|
||||
options.UseSqlite(connectionString, b => b.MigrationsAssembly("Hua.Todo.Application")));
|
||||
services.AddScoped<ITaskRepository, TaskRepository>();
|
||||
services.AddScoped<ITaskService, TaskService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using Hua.Todo.Application.Interfaces;
|
||||
using Hua.Todo.Application.Models;
|
||||
using Hua.Todo.Core.Entities;
|
||||
using Hua.Todo.Core.Interfaces;
|
||||
|
||||
namespace Hua.Todo.Application.Services;
|
||||
|
||||
public class TaskService : ITaskService
|
||||
{
|
||||
private readonly ITaskRepository _taskRepository;
|
||||
|
||||
public TaskService(ITaskRepository taskRepository)
|
||||
{
|
||||
_taskRepository = taskRepository;
|
||||
}
|
||||
|
||||
public async Task<List<TaskDto>> GetAllTasksAsync()
|
||||
{
|
||||
var tasks = await _taskRepository.GetAllAsync();
|
||||
return tasks.Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<TaskDto?> GetTaskByIdAsync(int id)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(id);
|
||||
return task != null ? MapToDto(task) : null;
|
||||
}
|
||||
|
||||
public async Task<List<TaskDto>> GetActiveTasksAsync()
|
||||
{
|
||||
var allTasks = await _taskRepository.GetAllAsync();
|
||||
return allTasks.Where(t => !t.IsCompleted).Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<TaskDto>> GetCompletedTasksAsync()
|
||||
{
|
||||
var allTasks = await _taskRepository.GetAllAsync();
|
||||
return allTasks.Where(t => t.IsCompleted).Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<TaskDto> CreateTaskAsync(CreateTaskDto dto)
|
||||
{
|
||||
var task = new TaskEntity
|
||||
{
|
||||
Title = dto.Title,
|
||||
Priority = dto.Priority,
|
||||
IsCompleted = false,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
ParentTaskId = dto.ParentTaskId
|
||||
};
|
||||
|
||||
var createdTask = await _taskRepository.AddAsync(task);
|
||||
return MapToDto(createdTask);
|
||||
}
|
||||
|
||||
public async Task<TaskDto> UpdateTaskAsync(UpdateTaskDto dto)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(dto.Id);
|
||||
if (task == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"Task with ID {dto.Id} not found");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
{
|
||||
task.Title = dto.Title;
|
||||
}
|
||||
|
||||
if (dto.Priority.HasValue)
|
||||
{
|
||||
task.Priority = dto.Priority.Value;
|
||||
}
|
||||
|
||||
task.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
var updatedTask = await _taskRepository.UpdateAsync(task);
|
||||
return MapToDto(updatedTask);
|
||||
}
|
||||
|
||||
public async Task<TaskDto> ToggleCompleteAsync(int id)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(id);
|
||||
if (task == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"Task with ID {id} not found");
|
||||
}
|
||||
|
||||
task.IsCompleted = !task.IsCompleted;
|
||||
task.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
var updatedTask = await _taskRepository.UpdateAsync(task);
|
||||
return MapToDto(updatedTask);
|
||||
}
|
||||
|
||||
public async Task DeleteTaskAsync(int id)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(id);
|
||||
if (task == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"Task with ID {id} not found");
|
||||
}
|
||||
|
||||
await _taskRepository.DeleteAsync(id);
|
||||
}
|
||||
|
||||
public async Task<List<TaskDto>> GetSubTasksAsync(int parentTaskId)
|
||||
{
|
||||
var allTasks = await _taskRepository.GetAllAsync();
|
||||
return allTasks.Where(t => t.ParentTaskId == parentTaskId).Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
private TaskDto MapToDto(TaskEntity task)
|
||||
{
|
||||
return new TaskDto
|
||||
{
|
||||
Id = task.Id,
|
||||
Title = task.Title,
|
||||
Priority = task.Priority,
|
||||
IsCompleted = task.IsCompleted,
|
||||
CreatedAt = task.CreatedAt,
|
||||
UpdatedAt = task.UpdatedAt,
|
||||
ParentTaskId = task.ParentTaskId,
|
||||
SubTasks = task.SubTasks.Select(MapToDto).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user