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:为端点生成唯一标识符
设计特点
- 数据规范化:确保数据一致性(大小写、空格、排序)
- 去重处理:通过分组和键值合并避免重复数据
- 缓存策略:读操作缓存,写操作失效缓存
- 增量更新:智能识别新增、更新和过时的路由
- 位运算合并:使用
|操作符合并参数来源枚举
使用场景
这个服务通常用于:
- 自动发现应用程序中的所有路由
- 验证 SEO 元数据中的路由配置是否有效
- 维护一个中心化的路由目录用于管理和查询
AI 正在分析代码…
评论加载中...