Skip to content

Commit 19d7bd2

Browse files
committed
ownership-semantics-lab: the dependency-identity falsifier sources (research-only)
Three builds of one library (fresh 1.0.0.0, cached 1.0.0.0, cached 2.0.0.0) and an app compiled against the fresh one; substituting a same-version patch in the app directory or loading a variant through a plugin AssemblyLoadContext changes the effective ownership semantics while App.deps.json still names Lib/1.0.0.0. Evidence in Own.NET-paperwork paper-eval/ownership-lab/depid-v1.json. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Am9eQwzNfbugH72eVKetC2
1 parent 4417fe1 commit 19d7bd2

9 files changed

Lines changed: 103 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings></PropertyGroup>
2+
<ItemGroup><Reference Include="Lib"><HintPath>../LibA/bin/Release/net8.0/Lib.dll</HintPath></Reference></ItemGroup></Project>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.Reflection;
2+
using System.Runtime.Loader;
3+
using Lib;
4+
// The APP was compiled against Lib A (Make() returns a fresh owned Res each call) and disposes each result: correct
5+
// under A. If the runtime substitutes a same-version patch (B) or a major bump (C) whose Make() returns a shared
6+
// cached Res, the second Make() hands back an already-disposed object: the effective semantics differ.
7+
static string Facets(Assembly a)
8+
{
9+
var n = a.GetName();
10+
var info = a.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
11+
return $"name={n.Name} version={n.Version} pkt={Convert.ToHexString(n.GetPublicKeyToken() ?? Array.Empty<byte>()).ToLowerInvariant()} mvid={a.ManifestModule.ModuleVersionId} info={info} path={a.Location}";
12+
}
13+
var mode = args.Length > 0 ? args[0] : "direct";
14+
if (mode == "direct")
15+
{
16+
var a = Factory.Make(); a.Dispose();
17+
var b = Factory.Make();
18+
Console.WriteLine($"[direct] loaded: {Facets(typeof(Factory).Assembly)}");
19+
Console.WriteLine($"[direct] flavor={Factory.Flavor} second_make_already_disposed={b.Disposed} same_instance={ReferenceEquals(a, b)} live={Res.Live}");
20+
b.Dispose();
21+
}
22+
else
23+
{
24+
// plugin-style load through a custom AssemblyLoadContext + AssemblyDependencyResolver rooted at a plugin dir
25+
var dir = Path.GetFullPath(args[1]);
26+
var alc = new PluginContext(Path.Combine(dir, "Lib.dll"));
27+
var asm = alc.LoadFromAssemblyName(new AssemblyName("Lib"));
28+
var factory = asm.GetType("Lib.Factory")!;
29+
var make = factory.GetMethod("Make")!;
30+
var r1 = (IDisposable)make.Invoke(null, null)!; r1.Dispose();
31+
var r2 = make.Invoke(null, null)!;
32+
var disposed = (bool)r2.GetType().GetProperty("Disposed")!.GetValue(r2)!;
33+
Console.WriteLine($"[plugin] loaded: {Facets(asm)}");
34+
Console.WriteLine($"[plugin] flavor={factory.GetProperty("Flavor")!.GetValue(null)} second_make_already_disposed={disposed} same_instance={ReferenceEquals(r1, r2)} default_alc_lib={Facets(typeof(Factory).Assembly)}");
35+
}
36+
sealed class PluginContext : AssemblyLoadContext
37+
{
38+
readonly AssemblyDependencyResolver _resolver;
39+
public PluginContext(string mainAssembly) : base(isCollectible: true) => _resolver = new AssemblyDependencyResolver(mainAssembly);
40+
protected override Assembly? Load(AssemblyName name)
41+
{
42+
var p = _resolver.ResolveAssemblyToPath(name);
43+
return p is null ? null : LoadFromAssemblyPath(p);
44+
}
45+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
namespace Lib;
3+
public sealed class Res : IDisposable
4+
{
5+
public static int Live;
6+
public int Id { get; }
7+
public bool Disposed { get; private set; }
8+
public Res(int id) { Id = id; Live++; }
9+
public void Dispose() { if (!Disposed) { Disposed = true; Live--; } }
10+
}
11+
public static class Factory
12+
{
13+
public static string Flavor => "A-fresh-1.0.0";
14+
static int s_next; public static Res Make() => new Res(++s_next);
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework><AssemblyName>Lib</AssemblyName><RootNamespace>Lib</RootNamespace><AssemblyVersion>1.0.0.0</AssemblyVersion><FileVersion>1.0.0.0</FileVersion><InformationalVersion>A-fresh-1.0.0</InformationalVersion><Nullable>enable</Nullable><Deterministic>true</Deterministic></PropertyGroup></Project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
namespace Lib;
3+
public sealed class Res : IDisposable
4+
{
5+
public static int Live;
6+
public int Id { get; }
7+
public bool Disposed { get; private set; }
8+
public Res(int id) { Id = id; Live++; }
9+
public void Dispose() { if (!Disposed) { Disposed = true; Live--; } }
10+
}
11+
public static class Factory
12+
{
13+
public static string Flavor => "B-cached-1.0.1";
14+
static Res? s_shared; public static Res Make() => s_shared ??= new Res(1);
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework><AssemblyName>Lib</AssemblyName><RootNamespace>Lib</RootNamespace><AssemblyVersion>1.0.0.0</AssemblyVersion><FileVersion>1.0.0.0</FileVersion><InformationalVersion>B-cached-1.0.1</InformationalVersion><Nullable>enable</Nullable><Deterministic>true</Deterministic></PropertyGroup></Project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
namespace Lib;
3+
public sealed class Res : IDisposable
4+
{
5+
public static int Live;
6+
public int Id { get; }
7+
public bool Disposed { get; private set; }
8+
public Res(int id) { Id = id; Live++; }
9+
public void Dispose() { if (!Disposed) { Disposed = true; Live--; } }
10+
}
11+
public static class Factory
12+
{
13+
public static string Flavor => "C-cached-2.0.0";
14+
static Res? s_shared; public static Res Make() => s_shared ??= new Res(1);
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework><AssemblyName>Lib</AssemblyName><RootNamespace>Lib</RootNamespace><AssemblyVersion>2.0.0.0</AssemblyVersion><FileVersion>2.0.0.0</FileVersion><InformationalVersion>C-cached-2.0.0</InformationalVersion><Nullable>enable</Nullable><Deterministic>true</Deterministic></PropertyGroup></Project>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# ownership-semantics-lab: runtime-effective dependency identity falsifier
2+
3+
Three builds of one library with an identical public surface — `LibA` (assembly 1.0.0.0, `Make()` returns a fresh
4+
owned resource), `LibB` (assembly 1.0.0.0, a "patch" whose `Make()` returns a cached shared resource), `LibC`
5+
(assembly 2.0.0.0, cached) — and an `App` compiled against `LibA` that disposes each `Make()` result. Substituting
6+
`LibB` or `LibC` in the app directory (no rebuild) or loading them through a plugin `AssemblyLoadContext` changes the
7+
effective ownership semantics while `App.deps.json` still says `Lib/1.0.0.0`. Evidence in Own.NET-paperwork
8+
`paper-eval/ownership-lab/depid-v1.json`. Research-only; not part of any build.

0 commit comments

Comments
 (0)