Skip to content

ElBruno.LocalLLMs 0.20.11 ships AssemblyVersion 0.20.9.0, breaking BlazorComponents/Rag 0.20.11 at startup #49

Description

@elbruno

Summary

The ElBruno.LocalLLMs 0.20.11 NuGet package ships ElBruno.LocalLLMs.dll stamped with
AssemblyVersion 0.20.9.0, while the sibling packages released at the same time
(ElBruno.LocalLLMs.BlazorComponents and ElBruno.LocalLLMs.Rag 0.20.11) were compiled against
ElBruno.LocalLLMs, Version=0.20.11.0.

.NET rolls assembly references forward but never backward, so the reference cannot bind and
any app that uses the core package together with BlazorComponents or Rag crashes at startup.

Impact

Hard startup crash, no graceful degradation:

Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly
'ElBruno.LocalLLMs, Version=0.20.11.0, Culture=neutral, PublicKeyToken=null'.
The system cannot find the file specified.
File name: 'ElBruno.LocalLLMs, Version=0.20.11.0, Culture=neutral, PublicKeyToken=null'
   at ElBruno.LocalLLMs.BlazorComponents.Extensions.BlazorComponentsServiceExtensions.AddLocalLLMsBlazorComponents(IServiceCollection services, Action`1 configure)
   at Program.<Main>$(String[] args)

The message is misleading — the file is present in the output folder; it is the version
downgrade that fails to bind.

Evidence

Reading AssemblyVersion straight out of the packages in ~/.nuget/packages:

Package Package version AssemblyVersion
ElBruno.LocalLLMs 0.20.9 0.20.9.0 ✅
ElBruno.LocalLLMs.BlazorComponents 0.20.9 0.20.9.0 ✅
ElBruno.LocalLLMs 0.20.10 0.20.10.0 ✅
ElBruno.LocalLLMs.BlazorComponents 0.20.10 0.20.10.0 ✅
ElBruno.LocalLLMs 0.20.11 0.20.9.0 ❌
ElBruno.LocalLLMs.BlazorComponents 0.20.11 0.20.11.0 ✅
ElBruno.LocalLLMs.Rag 0.20.11 0.20.11.0 ✅

Reproduce:

$root = "$env:USERPROFILE\.nuget\packages"
foreach ($v in '0.20.9','0.20.10','0.20.11') {
  foreach ($p in 'elbruno.localllms','elbruno.localllms.blazorcomponents') {
    $f = Get-ChildItem "$root\$p\$v\lib\*\*.dll" -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($f) {
      $n = [System.Reflection.AssemblyName]::GetAssemblyName($f.FullName)
      "{0,-40} pkg={1,-8} AssemblyVersion={2}" -f $p, $v, $n.Version
    }
  }
}

Note the FileVersion is also 0.20.9.0, so it looks like the version stamp was not
propagated to the core project for this release (the release was cut from a branch/state where
Version/VersionPrefix was still 0.20.9, or the property is only applied to the satellite
projects).

Steps to reproduce

  1. Create a .NET 8/10 Blazor Server app.
  2. dotnet add package ElBruno.LocalLLMs --version 0.20.11
  3. dotnet add package ElBruno.LocalLLMs.BlazorComponents --version 0.20.11
  4. Call builder.Services.AddLocalLLMsBlazorComponents(); in Program.cs.
  5. dotnet runFileNotFoundException before the host starts.

Expected

All packages published under the same release stamp a matching AssemblyVersion, so
ElBruno.LocalLLMs 0.20.11 exposes AssemblyVersion 0.20.11.0.

Suggested fix

  • Set Version / VersionPrefix in Directory.Build.props (or a shared Versions.props) so
    every packable project — including ElBruno.LocalLLMs itself — is stamped from one source.

  • Add a release check that fails if any packed assembly's AssemblyVersion does not match its
    package version. A few lines are enough:

    Get-ChildItem artifacts\*.nupkg | ForEach-Object { <# expand, compare AssemblyName.Version to package version #> }
  • Consider setting <AssemblyVersion>$(VersionPrefix)</AssemblyVersion> explicitly rather than
    relying on the default derivation, since a stale AssemblyVersion is invisible in
    dotnet list package and only shows up at runtime.

Workaround for consumers

Until a fixed package ships, an AssemblyLoadContext.Default.Resolving handler registered from
a [ModuleInitializer] (so it runs before Main is JIT-compiled) can bind the request to the
DLL that is actually in the output folder:

internal static class LocalLLMsAssemblyVersionShim
{
    private const string AssemblyName = "ElBruno.LocalLLMs";

    [ModuleInitializer]
    internal static void Initialize() => AssemblyLoadContext.Default.Resolving += Resolve;

    private static Assembly? Resolve(AssemblyLoadContext context, AssemblyName requested)
    {
        if (!string.Equals(requested.Name, AssemblyName, StringComparison.OrdinalIgnoreCase))
            return null;

        var path = Path.Combine(AppContext.BaseDirectory, $"{AssemblyName}.dll");
        if (!File.Exists(path))
            return null;

        var loaded = context.Assemblies.FirstOrDefault(
            a => string.Equals(a.GetName().Name, AssemblyName, StringComparison.OrdinalIgnoreCase));

        return loaded ?? context.LoadFromAssemblyPath(path);
    }
}

Environment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions