@@ -83,6 +83,20 @@ public static bool HasUpdate(
8383 return serverVersions . Max ( ) > local ;
8484 }
8585
86+ /// <summary>Pre-parses versions for a list of assets to avoid repeated Semver.TryParse calls.</summary>
87+ private static Dictionary < DownloadAsset , SemVersion ? > PreParseVersions ( IEnumerable < DownloadAsset > assets )
88+ {
89+ var map = new Dictionary < DownloadAsset , SemVersion ? > ( ) ;
90+ foreach ( var a in assets )
91+ {
92+ // Custom IDownloadSource implementations could return duplicate records;
93+ // silently accept the first occurrence rather than throwing.
94+ if ( ! map . ContainsKey ( a ) )
95+ map [ a ] = ParseVersion ( a . Version ) ;
96+ }
97+ return map ;
98+ }
99+
86100 /// <summary>
87101 /// Builds a download plan with AppType-aware version filtering.
88102 /// Client-type assets are compared against <paramref name="clientVersion"/>.
@@ -107,6 +121,16 @@ public static DownloadPlan Build(
107121 var parsedUpgrade = ParseVersion ( upgradeClientVersion ) ?? parsedClient ;
108122 var uv = parsedUpgrade . Value ;
109123
124+ // Pre-parse all asset versions to avoid repeated Semver.TryParse calls.
125+ var versionMap = PreParseVersions ( assets ) ;
126+
127+ // Helper: safe lookup that matches netstandard2.0 (no GetValueOrDefault).
128+ SemVersion ? Lookup ( DownloadAsset a )
129+ {
130+ versionMap . TryGetValue ( a , out var sv ) ;
131+ return sv ;
132+ }
133+
110134 // 1. Filter out frozen packages
111135 var active = assets
112136 . Where ( a => ! a . IsFreeze )
@@ -122,23 +146,22 @@ public static DownloadPlan Build(
122146 var candidates = active
123147 . Where ( a =>
124148 {
125- if ( ! Semver . TryParse ( a . Version , out var pv ) ) return false ;
149+ var pv = Lookup ( a ) ;
150+ if ( pv == null ) return false ;
126151
127152 var localVersion = ( a . AppType == ( int ) AppType . Upgrade )
128153 ? uv
129154 : cv ;
130155
131- return pv > localVersion ;
156+ return pv . Value > localVersion ;
132157 } )
133158 . Where ( a => IsCompatible ( a . MinClientVersion , clientVersion ) )
134- . OrderBy ( a => { Semver . TryParse ( a . Version , out var sv ) ; return sv ; } )
159+ . OrderBy ( a => Lookup ( a ) )
135160 . ToList ( ) ;
136161
137162 if ( candidates . Count == 0 ) return DownloadPlan . Empty ;
138163
139- // Separate chain vs full packages.
140- // Treat Unspecified (0) as Chain for backward compatibility with older
141- // servers that do not set PackageType yet.
164+ // 4. Separate chain vs full packages.
142165 var chainCandidates = candidates
143166 . Where ( a => a . PackageType == ( int ) Configuration . PackageType . Chain
144167 || a . PackageType == ( int ) Configuration . PackageType . Unspecified )
@@ -149,62 +172,50 @@ public static DownloadPlan Build(
149172 . ToList ( ) ;
150173
151174 // ── Chain vs Full size-based decision ──
152- // If a full replacement package is available and the total chain download
153- // size approaches or exceeds the full package size, skip chain and use full.
154175 if ( chainCandidates . Count > 0 && fullCandidates . Count > 0 )
155176 {
156- // Pick the latest full package (highest version) across all AppTypes
157177 var bestFull = fullCandidates
158- . OrderByDescending ( a => { Semver . TryParse ( a . Version , out var sv ) ; return sv ; } )
178+ . OrderByDescending ( a => Lookup ( a ) )
159179 . First ( ) ;
160180
161- // Only compare against chain packages of the same AppType as bestFull.
162- // Mixing Client and Upgrade sizes together could trigger incorrect switching.
163181 long chainTotal = chainCandidates
164182 . Where ( a => a . AppType == bestFull . AppType )
165183 . Sum ( a => a . Size ) ;
166184 var threshold = ( long ) ( bestFull . Size * 0.8 ) ;
167185
168186 if ( chainTotal >= threshold )
169187 {
170- // Chain is too expensive — use full package instead.
171- // Supplement with chain packages for other AppTypes not covered by full.
172188 GeneralTracer . Info ( $ "DownloadPlanBuilder: chain total { chainTotal } >= 80% of full size { bestFull . Size } , switching to full package { bestFull . Name } ") ;
189+ var bestFullSv = Lookup ( bestFull ) ;
173190 var planAssets = new List < DownloadAsset > { bestFull } ;
174191 planAssets . AddRange ( chainCandidates
175192 . Where ( a => a . AppType != bestFull . AppType
176- || ( Semver . TryParse ( a . Version , out var av )
177- && Semver . TryParse ( bestFull . Version , out var fv )
178- && av > fv ) )
179- . OrderBy ( a => { Semver . TryParse ( a . Version , out var sv ) ; return sv ; } ) ) ;
193+ || ( Lookup ( a ) is { } av && bestFullSv != null && av > bestFullSv ) )
194+ . OrderBy ( a => Lookup ( a ) ) ) ;
180195 return new DownloadPlan ( planAssets , isForcibly ) ;
181196 }
182197 }
183198
184199 // ── Chain plan with fallback fulls ──
185- // Use chain packages normally. Attach FallbackFull* info to each chain entry
186- // so that if a chain patch fails, AbstractStrategy can fall back to full.
187200 if ( fullCandidates . Count > 0 )
188201 {
189202 var fallbackFulls = new List < DownloadAsset > ( ) ;
190203
191204 var chainWithFallback = chainCandidates
192205 . Select ( chain =>
193206 {
194- // Find a matching full: same AppType + same Version (or closest)
195207 var match = fullCandidates
196208 . Where ( f => f . AppType == chain . AppType )
197- . OrderBy ( f => { Semver . TryParse ( f . Version , out var sv ) ; return sv ; } )
209+ . OrderBy ( f => Lookup ( f ) )
198210 . FirstOrDefault ( f =>
199211 {
200- if ( ! Semver . TryParse ( f . Version , out var fv ) ) return false ;
201- if ( ! Semver . TryParse ( chain . Version , out var cv ) ) return false ;
202- return fv >= cv ;
212+ var fv = Lookup ( f ) ;
213+ var cv = Lookup ( chain ) ;
214+ return fv != null && cv != null && fv . Value >= cv . Value ;
203215 } ) ;
204216
205217 if ( match != null )
206218 {
207- // Add matching full to the fallback list once
208219 if ( ! fallbackFulls . Any ( f => f . Url == match . Url ) )
209220 fallbackFulls . Add ( match ) ;
210221
@@ -226,7 +237,6 @@ public static DownloadPlan Build(
226237 } ;
227238 }
228239
229- // No full packages at all: return chain packages as-is
230240 return new DownloadPlan ( chainCandidates , isForcibly ) ;
231241 }
232242
@@ -245,11 +255,7 @@ public static DownloadPlan Build(IEnumerable<DownloadAsset> assets, string curre
245255
246256 /// <summary>
247257 /// Checks whether the specified MinClientVersion is compatible with the current client version.
248- /// If a package's MinClientVersion is higher than the current version, the package is not applicable.
249258 /// </summary>
250- /// <param name="minClientVersion">The minimum client version required by the package. If null or empty, the package is considered compatible.</param>
251- /// <param name="currentVersion">The current client version string.</param>
252- /// <returns>True if the current version meets or exceeds the minimum requirement; otherwise false.</returns>
253259 internal static bool IsCompatible ( string ? minClientVersion , string currentVersion )
254260 {
255261 if ( string . IsNullOrEmpty ( minClientVersion ) ) return true ;
@@ -260,8 +266,6 @@ internal static bool IsCompatible(string? minClientVersion, string currentVersio
260266 }
261267
262268 /// <summary>Parses a version string and returns null if the string cannot be parsed.</summary>
263- /// <param name="version">The version string to parse.</param>
264- /// <returns>A parsed <see cref="SemVersion"/> value, or null if parsing fails.</returns>
265269 internal static SemVersion ? ParseVersion ( string ? version )
266270 {
267271 if ( string . IsNullOrWhiteSpace ( version ) ) return null ;
0 commit comments