using Dpz.Core.Public.Entity.Seo;

namespace Dpz.Core.Service.RepositoryServiceImpl;

public class PageRouteCatalogService(IRepository<PageRouteDefinition> repository, IMapper mapper)
    : IPageRouteCatalogService
{
    [Cache(ExpirationSeconds = ExpirationTime.Week)]
    public async Task<IList<VmPageRouteDefinition>> GetRoutesAsync(
        bool activeOnly = true,
        CancellationToken cancellationToken = default
    )
    {
        var query = repository.SearchFor(x => true);
        if (activeOnly)
        {
            query = query.Where(x => x.IsActive);
        }

        var list = await query
            .OrderBy(x => x.Controller)
            .ThenBy(x => x.Action)
            .ToListAsync(cancellationToken);
        return mapper.Map<List<VmPageRouteDefinition>>(list);
    }

    public async Task<PageRouteValidationResult> ValidateRouteAsync(
        PageMetadataRoute route,
        CancellationToken cancellationToken = default
    )
    {
        var normalizedRoute = route.Normalize();
        if (string.IsNullOrWhiteSpace(normalizedRoute.Controller))
        {
            return PageRouteValidationResult.Fail("route.controller is required");
        }

        if (string.IsNullOrWhiteSpace(normalizedRoute.Action))
        {
            return PageRouteValidationResult.Fail("route.action is required");
        }

        var emptyParameters = normalizedRoute
            .Parameters.Where(x => string.IsNullOrWhiteSpace(x.Value))
            .Select(x => x.Key)
            .ToList();
        if (emptyParameters.Count > 0)
        {
            return PageRouteValidationResult.Fail(
                $"route parameters cannot be empty: {string.Join(",", emptyParameters)}"
            );
        }

        var routeKey = normalizedRoute.BuildActionKey();
        var routeDefinition = await repository
            .SearchFor(x => x.RouteKey == routeKey && x.IsActive)
            .FirstOrDefaultAsync(cancellationToken);
        if (routeDefinition == null)
        {
            return PageRouteValidationResult.Fail("route is not found in page route catalog");
        }

        var allowedParameters = routeDefinition
            .Parameters.Where(x => x.Source != PageRouteParameterSource.None)
            .Select(x => x.Name)
            .Where(x => !string.IsNullOrWhiteSpace(x))
            .Select(x => x.ToLowerInvariant())
            .ToHashSet(StringComparer.Ordinal);
        var invalidParameters = normalizedRoute
            .Parameters.Keys.Where(x => !allowedParameters.Contains(x))
            .ToList();
        if (invalidParameters.Count > 0)
        {
            return PageRouteValidationResult.Fail(
                $"route parameters are invalid: {string.Join(",", invalidParameters)}"
            );
        }

        return PageRouteValidationResult.Success;
    }

    [InvalidateCache(Methods = [nameof(GetRoutesAsync)])]
    public async Task SaveScannedRoutesAsync(
        IReadOnlyCollection<VmSavePageRouteDefinition> routes,
        CancellationToken cancellationToken = default
    )
    {
        var scanTime = DateTime.Now;
        var normalizedRoutes = routes
            .Select(NormalizeRoute)
            .Where(x =>
                !string.IsNullOrWhiteSpace(x.Controller) && !string.IsNullOrWhiteSpace(x.Action)
            )
            .GroupBy(x => PageMetadataRoute.BuildActionKey(x.Area, x.Controller, x.Action))
            .Select(x => MergeRouteDefinitions(x.Key, x, scanTime))
            .ToList();
        var scannedKeys = normalizedRoutes
            .Select(x => x.RouteKey)
            .ToHashSet(StringComparer.Ordinal);

        foreach (var route in normalizedRoutes)
        {
            var exists = await repository
                .SearchFor(x => x.RouteKey == route.RouteKey)
                .FirstOrDefaultAsync(cancellationToken);
            if (exists == null)
            {
                await repository.InsertAsync(route, cancellationToken);
                continue;
            }

            var update = Builders<PageRouteDefinition>
                .Update.Set(x => x.Area, route.Area)
                .Set(x => x.Controller, route.Controller)
                .Set(x => x.Action, route.Action)
                .Set(x => x.HttpMethods, route.HttpMethods)
                .Set(x => x.Endpoints, route.Endpoints)
                .Set(x => x.Parameters, route.Parameters)
                .Set(x => x.IsActive, true)
                .Set(x => x.LastScannedAt, scanTime);
            await repository.UpdateAsync(x => x.Id == exists.Id, update, cancellationToken);
        }

        var activeRoutes = await repository
            .SearchFor(x => x.IsActive)
            .ToListAsync(cancellationToken);
        var staleRoutes = activeRoutes.Where(x => !scannedKeys.Contains(x.RouteKey)).ToList();
        foreach (var staleRoute in staleRoutes)
        {
            var update = Builders<PageRouteDefinition>
                .Update.Set(x => x.IsActive, false)
                .Set(x => x.LastScannedAt, scanTime);
            await repository.UpdateAsync(x => x.Id == staleRoute.Id, update, cancellationToken);
        }
    }

    [InvalidateCache(Methods = [nameof(GetRoutesAsync)])]
    public Task RefreshCacheAsync(CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    private static VmSavePageRouteDefinition NormalizeRoute(VmSavePageRouteDefinition route)
    {
        return new VmSavePageRouteDefinition
        {
            Area = PageMetadataRoute.NormalizeSegment(route.Area),
            Controller = PageMetadataRoute.NormalizeSegment(route.Controller) ?? string.Empty,
            Action = PageMetadataRoute.NormalizeSegment(route.Action) ?? string.Empty,
            HttpMethods = route
                .HttpMethods.Select(x => x.Trim().ToUpperInvariant())
                .Where(x => !string.IsNullOrWhiteSpace(x))
                .Distinct(StringComparer.Ordinal)
                .OrderBy(x => x, StringComparer.Ordinal)
                .ToList(),
            Endpoints = route
                .Endpoints.Select(NormalizeEndpoint)
                .Where(x => !string.IsNullOrWhiteSpace(x.Template))
                .GroupBy(BuildEndpointKey, StringComparer.Ordinal)
                .Select(x => x.First())
                .OrderBy(x => x.Template, StringComparer.OrdinalIgnoreCase)
                .ToList(),
            Parameters = route
                .Parameters.Select(NormalizeParameter)
                .Where(x => !string.IsNullOrWhiteSpace(x.Name))
                .GroupBy(x => x.Name, StringComparer.Ordinal)
                .Select(MergeParameters)
                .OrderBy(x => x.Name, StringComparer.Ordinal)
                .ToList(),
        };
    }

    private static VmPageRouteParameterDefinition NormalizeParameter(
        VmPageRouteParameterDefinition parameter
    )
    {
        return new VmPageRouteParameterDefinition
        {
            Name = PageMetadataRoute.NormalizeSegment(parameter.Name) ?? string.Empty,
            TypeName = parameter.TypeName,
            Source = parameter.Source,
            IsOptional = parameter.IsOptional,
        };
    }

    private static VmPageRouteEndpointDefinition NormalizeEndpoint(
        VmPageRouteEndpointDefinition endpoint
    )
    {
        return new VmPageRouteEndpointDefinition
        {
            RouteName = string.IsNullOrWhiteSpace(endpoint.RouteName)
                ? null
                : endpoint.RouteName.Trim(),
            Template = endpoint.Template.Trim(),
            HttpMethods = endpoint
                .HttpMethods.Select(x => x.Trim().ToUpperInvariant())
                .Where(x => !string.IsNullOrWhiteSpace(x))
                .Distinct(StringComparer.Ordinal)
                .OrderBy(x => x, StringComparer.Ordinal)
                .ToList(),
            Defaults = NormalizeValues(endpoint.Defaults),
            RequiredValues = NormalizeValues(endpoint.RequiredValues),
        };
    }

    private static Dictionary<string, string> NormalizeValues(
        IReadOnlyDictionary<string, string> values
    )
    {
        return values
            .Where(x => !string.IsNullOrWhiteSpace(x.Key))
            .Select(x => new KeyValuePair<string, string>(
                PageMetadataRoute.NormalizeSegment(x.Key) ?? string.Empty,
                x.Value?.Trim() ?? string.Empty
            ))
            .GroupBy(x => x.Key, StringComparer.Ordinal)
            .Select(x => x.Last())
            .OrderBy(x => x.Key, StringComparer.Ordinal)
            .ToDictionary(x => x.Key, x => x.Value, StringComparer.Ordinal);
    }

    private static string BuildEndpointKey(VmPageRouteEndpointDefinition endpoint)
    {
        return string.Join(
            "|",
            endpoint.RouteName ?? string.Empty,
            endpoint.Template.ToLowerInvariant(),
            string.Join(",", endpoint.HttpMethods),
            string.Join(",", endpoint.Defaults.Select(x => $"{x.Key}={x.Value}")),
            string.Join(",", endpoint.RequiredValues.Select(x => $"{x.Key}={x.Value}"))
        );
    }

    private static VmPageRouteParameterDefinition MergeParameters(
        IGrouping<string, VmPageRouteParameterDefinition> parameters
    )
    {
        return new VmPageRouteParameterDefinition
        {
            Name = parameters.Key,
            TypeName = parameters
                .Select(x => x.TypeName)
                .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x)),
            Source = parameters.Aggregate(
                PageRouteParameterSource.None,
                (source, parameter) => source | parameter.Source
            ),
            IsOptional = parameters.All(x => x.IsOptional),
        };
    }

    private static PageRouteDefinition MergeRouteDefinitions(
        string routeKey,
        IEnumerable<VmSavePageRouteDefinition> routes,
        DateTime scanTime
    )
    {
        var routeList = routes.ToList();
        var first = routeList[0];
        return new PageRouteDefinition
        {
            RouteKey = routeKey,
            Area = first.Area,
            Controller = first.Controller,
            Action = first.Action,
            HttpMethods = routeList
                .SelectMany(x => x.HttpMethods)
                .Distinct(StringComparer.Ordinal)
                .OrderBy(x => x, StringComparer.Ordinal)
                .ToList(),
            Endpoints = routeList
                .SelectMany(x => x.Endpoints)
                .GroupBy(BuildEndpointKey, StringComparer.Ordinal)
                .Select(x => new PageRouteEndpointDefinition
                {
                    RouteName = x.First().RouteName,
                    Template = x.First().Template,
                    HttpMethods = x.First().HttpMethods,
                    Defaults = x.First().Defaults,
                    RequiredValues = x.First().RequiredValues,
                })
                .OrderBy(x => x.Template, StringComparer.OrdinalIgnoreCase)
                .ToList(),
            Parameters = routeList
                .SelectMany(x => x.Parameters)
                .GroupBy(x => x.Name, StringComparer.Ordinal)
                .Select(x => new PageRouteParameterDefinition
                {
                    Name = x.Key,
                    TypeName = x.Select(p => p.TypeName)
                        .FirstOrDefault(p => !string.IsNullOrWhiteSpace(p)),
                    Source = x.Aggregate(
                        PageRouteParameterSource.None,
                        (source, parameter) => source | parameter.Source
                    ),
                    IsOptional = x.All(p => p.IsOptional),
                })
                .OrderBy(x => x.Name, StringComparer.Ordinal)
                .ToList(),
            IsActive = true,
            LastScannedAt = scanTime,
        };
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码说明

这是一个页面路由目录服务的实现类,主要用于管理和验证 ASP.NET Core 应用程序中的路由定义。

主要功能

1. GetRoutesAsync - 获取路由列表

[Cache(ExpirationSeconds = ExpirationTime.Week)]
public async Task<IList<VmPageRouteDefinition>> GetRoutesAsync(...)
  • 从数据库查询路由定义
  • 可选择只返回激活状态的路由
  • 按 Controller 和 Action 排序
  • 结果会被缓存一周

2. ValidateRouteAsync - 验证路由有效性

public async Task<PageRouteValidationResult> ValidateRouteAsync(...)

验证步骤包括:

  • 必填项检查:Controller 和 Action 不能为空
  • 参数完整性:所有参数值不能为空
  • 路由存在性:路由必须在目录中已定义且激活
  • 参数合法性:传入的参数必须在允许的参数列表中

3. SaveScannedRoutesAsync - 保存扫描的路由

[InvalidateCache(Methods = [nameof(GetRoutesAsync)])]
public async Task SaveScannedRoutesAsync(...)

这是核心方法,执行以下操作:

数据规范化

  • 标准化路由信息(Area、Controller、Action)
  • 去重并排序 HTTP 方法
  • 合并重复的端点和参数定义

数据库同步

  • 新路由:直接插入数据库
  • 已存在路由:更新其详细信息并标记为激活
  • 过时路由:标记为非激活状态(未在本次扫描中发现)

缓存失效

  • 操作完成后自动清除 GetRoutesAsync 的缓存

4. RefreshCacheAsync - 刷新缓存

[InvalidateCache(Methods = [nameof(GetRoutesAsync)])]
public Task RefreshCacheAsync(...)
  • 手动触发缓存清除
  • 实际上是个空操作,依靠特性来清缓存

辅助方法

规范化方法

  • NormalizeRoute:规范化整个路由定义
  • NormalizeParameter:规范化参数定义
  • NormalizeEndpoint:规范化端点定义
  • NormalizeValues:规范化键值对字典

合并方法

  • MergeParameters:合并同名参数的不同来源
  • MergeRouteDefinitions:合并相同路由的多个定义

工具方法

  • BuildEndpointKey:为端点生成唯一标识符

设计特点

  1. 数据规范化:确保数据一致性(大小写、空格、排序)
  2. 去重处理:通过分组和键值合并避免重复数据
  3. 缓存策略:读操作缓存,写操作失效缓存
  4. 增量更新:智能识别新增、更新和过时的路由
  5. 位运算合并:使用 | 操作符合并参数来源枚举

使用场景

这个服务通常用于:

  • 自动发现应用程序中的所有路由
  • 验证 SEO 元数据中的路由配置是否有效
  • 维护一个中心化的路由目录用于管理和查询
评论加载中...