< Summary

Information
Class: OzonOrdersWeb.Authorization.Discovery.PageDiscoveryService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Authorization/Discovery/PageDiscoveryService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 98
Coverable lines: 98
Total lines: 181
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 62
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
DiscoverAsync()0%930300%
SyncAllAsync()0%156120%
RefreshDisplayNamesFromViewsAsync()0%156120%
GetControllerTypes()0%620%
StripControllerSuffix(...)0%620%
GetActionName(...)0%2040%
BuildCode(...)0%620%
Key(...)0%620%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Authorization/Discovery/PageDiscoveryService.cs

#LineLine coverage
 1using System.Reflection;
 2using Microsoft.AspNetCore.Authorization;
 3using Microsoft.AspNetCore.Hosting;
 4using Microsoft.AspNetCore.Mvc;
 5using Microsoft.EntityFrameworkCore;
 6using Microsoft.Extensions.Caching.Memory;
 7using OzonDomains.Models.Authorization;
 8using OzonRepositories.Context;
 9
 10namespace OzonOrdersWeb.Authorization.Discovery;
 11
 12public sealed class PageDiscoveryService : IPageDiscoveryService
 13{
 14    private readonly ApplicationDbContext _db;
 15    private readonly IMemoryCache _cache;
 16    private readonly IWebHostEnvironment _env;
 17
 018    public PageDiscoveryService(ApplicationDbContext db, IMemoryCache cache, IWebHostEnvironment env)
 19    {
 020        _db = db;
 021        _cache = cache;
 022        _env = env;
 023    }
 24
 25    public async Task<List<DiscoveredPage>> DiscoverAsync(CancellationToken ct = default)
 26    {
 027        var existingCodes = (await _db.AppPages.AsNoTracking().Select(p => p.Code).ToListAsync(ct))
 028            .ToHashSet(StringComparer.OrdinalIgnoreCase);
 29
 30        // Дедупликация по Code (перегрузки Create(GET)/Create(POST) и наследуемые экшены дают одинаковый код).
 031        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 032        var result = new List<DiscoveredPage>();
 33
 034        foreach (var type in GetControllerTypes())
 35        {
 036            if (type.GetCustomAttribute<AllowAnonymousAttribute>() is not null) continue;
 037            if (type.GetCustomAttribute<ServiceEndpointAttribute>() is not null) continue;
 038            var area = type.GetCustomAttribute<AreaAttribute>()?.RouteValue;
 039            var controller = StripControllerSuffix(type.Name);
 40
 041            foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOn
 42            {
 043                if (method.IsSpecialName) continue;
 044                if (method.GetCustomAttribute<NonActionAttribute>() is not null) continue;
 045                if (method.GetCustomAttribute<AllowAnonymousAttribute>() is not null) continue;
 46                // Служебные лукапы страницами не считаем — иначе на них требуются права, которых нет ни у одной роли.
 047                if (method.GetCustomAttribute<ServiceEndpointAttribute>() is not null) continue;
 048                if (!typeof(IActionResult).IsAssignableFrom(method.ReturnType) &&
 049                    !typeof(Task<IActionResult>).IsAssignableFrom(method.ReturnType) &&
 050                    !method.ReturnType.Name.Contains("Action") &&
 051                    !method.ReturnType.Name.Contains("Task"))
 52                    continue;
 53
 054                var action = GetActionName(method);
 055                var code = BuildCode(area, controller, action);
 056                if (!seen.Add(code)) continue;
 57
 058                var fromView = RazorViewTitleExtractor.TryGetTitleFromConventionalView(
 059                    _env.ContentRootPath, area, controller, action);
 060                var displayName = !string.IsNullOrEmpty(fromView)
 061                    ? fromView!
 062                    : $"{controller} / {action}";
 63
 064                result.Add(new DiscoveredPage
 065                {
 066                    Code = code,
 067                    DisplayName = displayName,
 068                    Area = area,
 069                    Controller = controller,
 070                    Action = action,
 071                    ExistsInDb = existingCodes.Contains(code),
 072                });
 73            }
 74        }
 75        return result.OrderBy(r => r.ExistsInDb).ThenBy(r => r.Code).ToList();
 076    }
 77
 78    public async Task<int> SyncAllAsync(CancellationToken ct = default)
 79    {
 080        var found = await DiscoverAsync(ct);
 081        var added = 0;
 82
 83        // Коды страниц, которые должны отображаться в меню
 084        var pimNavCodes = new HashSet<string>(Studio2PimNavMenuCatalog.AllPimNavPageCodes, StringComparer.OrdinalIgnoreC
 85
 86        // Добавляем новые страницы и обновляем IsMenuItem для существующих
 087        var existingPages = await _db.AppPages.ToListAsync(ct);
 88        var existingByCode = existingPages.ToDictionary(p => p.Code, StringComparer.OrdinalIgnoreCase);
 89
 090        foreach (var d in found)
 91        {
 092            if (!d.ExistsInDb)
 93            {
 94                // Добавляем новую страницу
 095                var isMenuItem = pimNavCodes.Contains(d.Code);
 096                _db.AppPages.Add(new AppPage
 097                {
 098                    Code = d.Code,
 099                    DisplayName = d.DisplayName,
 0100                    Area = d.Area,
 0101                    Controller = d.Controller,
 0102                    Action = d.Action,
 0103                    IsActive = true,
 0104                    IsMenuItem = isMenuItem
 0105                });
 0106                added++;
 107            }
 0108            else if (existingByCode.TryGetValue(d.Code, out var existing))
 109            {
 110                // Обновляем IsMenuItem для существующей страницы если она в каталоге
 0111                var shouldBeMenuItem = pimNavCodes.Contains(d.Code);
 0112                if (existing.IsMenuItem != shouldBeMenuItem)
 113                {
 0114                    existing.IsMenuItem = shouldBeMenuItem;
 0115                    existing.UpdatedAt = DateTime.UtcNow;
 116                }
 117            }
 118        }
 119
 120        if (added > 0 || existingPages.Any(p => p.UpdatedAt > DateTime.UtcNow.AddSeconds(-1)))
 121        {
 0122            await _db.SaveChangesAsync(ct);
 0123            AutoPageAuthorizationFilter.InvalidatePageRegistry(_cache);
 124        }
 0125        return added;
 0126    }
 127
 128    public async Task<int> RefreshDisplayNamesFromViewsAsync(CancellationToken ct = default)
 129    {
 0130        var root = _env.ContentRootPath;
 0131        if (string.IsNullOrEmpty(root) || !Directory.Exists(root)) return 0;
 132
 0133        var pages = await _db.AppPages.ToListAsync(ct);
 0134        var updated = 0;
 0135        foreach (var p in pages)
 136        {
 0137            if (!RazorViewTitleExtractor.IsLegacyControllerActionLabel(p.Controller, p.Action, p.DisplayName)) continue;
 0138            var title = RazorViewTitleExtractor.TryGetTitleFromConventionalView(root, p.Area, p.Controller, p.Action);
 0139            if (string.IsNullOrEmpty(title)) continue;
 0140            p.DisplayName = title;
 0141            p.UpdatedAt = DateTime.UtcNow;
 0142            updated++;
 143        }
 0144        if (updated > 0)
 145        {
 0146            await _db.SaveChangesAsync(ct);
 0147            AutoPageAuthorizationFilter.InvalidatePageRegistry(_cache);
 148        }
 0149        return updated;
 0150    }
 151
 152    private static IEnumerable<Type> GetControllerTypes()
 153    {
 0154        return AppDomain.CurrentDomain.GetAssemblies()
 0155            .Where(a => !a.IsDynamic)
 0156            .SelectMany(a =>
 0157            {
 0158                try { return a.GetTypes(); }
 0159                catch { return Array.Empty<Type>(); }
 0160            })
 0161            .Where(t => typeof(ControllerBase).IsAssignableFrom(t) && !t.IsAbstract);
 162    }
 163
 164    private static string StripControllerSuffix(string name)
 0165        => name.EndsWith("Controller", StringComparison.Ordinal) ? name[..^"Controller".Length] : name;
 166
 167    private static string GetActionName(MethodInfo method)
 168    {
 0169        var attr = method.GetCustomAttribute<ActionNameAttribute>();
 0170        return attr?.Name ?? method.Name;
 171    }
 172
 173    private static string BuildCode(string? area, string controller, string action)
 174    {
 0175        var a = string.IsNullOrWhiteSpace(area) ? string.Empty : area + ".";
 0176        return $"{a}{controller}.{action}";
 177    }
 178
 179    private static string Key(string? area, string controller, string action)
 0180        => $"{(area ?? string.Empty).ToLowerInvariant()}|{controller.ToLowerInvariant()}|{action.ToLowerInvariant()}";
 181}