1+ using Microsoft . AspNetCore . Components ;
2+ using Microsoft . AspNetCore . Components . Web ;
3+ using Microsoft . AspNetCore . Hosting ;
4+ using Microsoft . AspNetCore . Http ;
5+ using Microsoft . Extensions . Hosting ;
6+ using Microsoft . Extensions . Options ;
7+ using System . Text . Json ;
8+
9+ namespace BlazorTS . Assets ;
10+
11+ /// <summary>
12+ /// Service for managing assets, handling asset path resolution in development and production environments.
13+ /// </summary>
14+ public class AssetService : IAssetService
15+ {
16+ private readonly IWebHostEnvironment _environment ;
17+ private readonly IHttpContextAccessor _httpContextAccessor ;
18+ private readonly AssetOptions _options ;
19+ private readonly Lazy < Dictionary < string , AssetManifestEntry > > _manifestCache ;
20+ private readonly Lazy < ImportMapDefinition ? > _importMapCache ;
21+
22+ /// <summary>
23+ /// Gets a value indicating whether the current environment is development.
24+ /// </summary>
25+ public bool IsDevelopment => _environment . IsDevelopment ( ) ;
26+
27+ /// <summary>
28+ /// Gets the import map definition (only available in production).
29+ /// </summary>
30+ public ImportMapDefinition ? ImportMap => IsDevelopment ? null : _importMapCache . Value ;
31+
32+ /// <summary>
33+ /// Initializes a new instance of the <see cref="AssetService"/> class.
34+ /// </summary>
35+ /// <param name="environment">The web host environment.</param>
36+ /// <param name="httpContextAccessor">The HTTP context accessor.</param>
37+ /// <param name="options">The asset options.</param>
38+ public AssetService (
39+ IWebHostEnvironment environment ,
40+ IHttpContextAccessor httpContextAccessor ,
41+ IOptions < AssetOptions > options )
42+ {
43+ _environment = environment ;
44+ _httpContextAccessor = httpContextAccessor ;
45+ _options = options . Value ;
46+
47+ // Lazily load the manifest and import map to improve startup performance.
48+ _manifestCache = new Lazy < Dictionary < string , AssetManifestEntry > > ( LoadManifest ) ;
49+ _importMapCache = new Lazy < ImportMapDefinition ? > ( CreateImportMap ) ;
50+ }
51+
52+ /// <summary>
53+ /// Gets the URL path for the specified asset key.
54+ /// </summary>
55+ /// <param name="assetKey">The asset key.</param>
56+ /// <returns>The full URL path of the asset.</returns>
57+ /// <exception cref="KeyNotFoundException">Thrown when the asset is not found in the manifest in a production environment.</exception>
58+ public string this [ string assetKey ]
59+ {
60+ get
61+ {
62+ return IsDevelopment
63+ ? GetDevelopmentAssetUrl ( assetKey )
64+ : GetProductionAssetUrl ( assetKey ) ;
65+ }
66+ }
67+
68+ private string GetDevelopmentAssetUrl ( string assetKey )
69+ {
70+ return $ "{ _options . DevServerUrl } /{ assetKey } ";
71+ }
72+
73+ private string GetProductionAssetUrl ( string assetKey )
74+ {
75+ if ( ! _manifestCache . Value . TryGetValue ( assetKey , out var manifestEntry ) )
76+ {
77+ throw new KeyNotFoundException ( $ "Asset not found in manifest: { assetKey } ") ;
78+ }
79+
80+ return NormalizeAssetPath ( manifestEntry . File ) ;
81+ }
82+
83+ private static string NormalizeAssetPath ( string filePath )
84+ {
85+ return filePath . StartsWith ( '/' ) ? filePath : $ "/{ filePath } ";
86+ }
87+
88+ private Dictionary < string , AssetManifestEntry > LoadManifest ( )
89+ {
90+ if ( IsDevelopment )
91+ {
92+ return new Dictionary < string , AssetManifestEntry > ( ) ;
93+ }
94+
95+ var manifestPath = Path . Combine ( _environment . WebRootPath , _options . ManifestPath ) ;
96+
97+ if ( ! File . Exists ( manifestPath ) )
98+ {
99+ throw new FileNotFoundException ( $ "Vite manifest file not found: { manifestPath } ") ;
100+ }
101+
102+ var manifestJson = File . ReadAllText ( manifestPath ) ;
103+ var deserializeOptions = new JsonSerializerOptions
104+ {
105+ PropertyNameCaseInsensitive = true
106+ } ;
107+
108+ return JsonSerializer . Deserialize < Dictionary < string , AssetManifestEntry > > ( manifestJson , deserializeOptions )
109+ ?? new Dictionary < string , AssetManifestEntry > ( ) ;
110+ }
111+
112+ private ImportMapDefinition ? CreateImportMap ( )
113+ {
114+ if ( IsDevelopment )
115+ {
116+ return null ;
117+ }
118+
119+ var manifestImports = ExtractManifestImports ( ) ;
120+ var endpointImports = GetEndpointImportMap ( ) ;
121+
122+ if ( manifestImports . Any ( ) || endpointImports is not null )
123+ {
124+ return CombineImportMaps ( manifestImports , endpointImports ) ;
125+ }
126+
127+ return null ;
128+ }
129+
130+ private Dictionary < string , string > ExtractManifestImports ( )
131+ {
132+ return _manifestCache . Value
133+ . Where ( entry => entry . Value . IsEntry == true && ! string . IsNullOrEmpty ( entry . Value . Src ) )
134+ . ToDictionary (
135+ entry => entry . Value . Src ! ,
136+ entry => NormalizeAssetPath ( entry . Value . File )
137+ ) ;
138+ }
139+
140+ private ImportMapDefinition ? GetEndpointImportMap ( )
141+ {
142+ return _httpContextAccessor . HttpContext ?
143+ . GetEndpoint ( ) ?
144+ . Metadata
145+ . GetMetadata < ImportMapDefinition > ( ) ;
146+ }
147+
148+ private static ImportMapDefinition CombineImportMaps (
149+ Dictionary < string , string > manifestImports ,
150+ ImportMapDefinition ? endpointImports )
151+ {
152+ var manifestImportMap = new ImportMapDefinition ( manifestImports , null , null ) ;
153+ var fallbackImportMap = endpointImports ?? new ImportMapDefinition ( null , null , null ) ;
154+
155+ return ImportMapDefinition . Combine ( manifestImportMap , fallbackImportMap ) ;
156+ }
157+ }
0 commit comments