diff --git a/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/GeneratedHintName.cs b/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/GeneratedHintName.cs
new file mode 100644
index 00000000..cda92649
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/GeneratedHintName.cs
@@ -0,0 +1,86 @@
+namespace EntityFrameworkCore.Projectables.Generator.Infrastructure;
+
+///
+/// Builds the hint name (the file name shown in the IDE) for a generated per-member source file.
+///
+/// The generated class name embeds the fully-qualified type name of every parameter, so for methods
+/// with many or deeply generic parameters it grows to hundreds of characters. Using that string
+/// directly as the file name produces paths that overflow limits such as Windows' MAX_PATH
+/// (~260) — the generator sub-folder prefix alone consumes ~200 characters — which crashes Visual
+/// Studio when browsing the files under Dependencies > Analyzers.
+///
+///
+/// The hint name is a pure IDE/filesystem artifact and is never read at runtime, so it can be shortened
+/// freely. This is distinct from the generated class name, which is a runtime contract (resolved
+/// by Assembly.GetType in the reflection fallback and embedded in the registry) and must never
+/// change here. Short names are returned unchanged; over-long names are rewritten to a readable prefix
+/// plus a deterministic hash of the full name so the result stays unique per member/overload.
+///
+///
+internal static class GeneratedHintName
+{
+ ///
+ /// Names whose base is at most this long are emitted unchanged, keeping the common case fully
+ /// readable and byte-identical to previous versions.
+ ///
+ private const int MaxBaseNameLength = 64;
+
+ ///
+ /// Upper bound on the readable head kept in the shortened form before the hash is appended.
+ ///
+ private const int MaxReadablePrefixLength = 40;
+
+ ///
+ /// Builds the .g.cs hint name for a generated member.
+ ///
+ ///
+ /// The name emitted before the .g.cs suffix today (the generated class name, optionally with
+ /// the open-generic -{typeParamCount} disambiguator). This is the uniqueness carrier: the hash
+ /// is computed over its entirety so distinct members can never collide.
+ ///
+ ///
+ /// The parameter-less identity head (namespace + nested classes + member name). It is a literal prefix
+ /// of and is kept, truncated if necessary, for human readability.
+ ///
+ public static string Build(string baseName, string readablePrefix)
+ {
+ if (baseName.Length <= MaxBaseNameLength)
+ {
+ return baseName + ".g.cs";
+ }
+
+ var prefix = readablePrefix.Length <= MaxReadablePrefixLength
+ ? readablePrefix
+ : readablePrefix.Substring(0, MaxReadablePrefixLength);
+
+ return prefix + "_" + Fnv1a64Hex(baseName) + ".g.cs";
+ }
+
+ ///
+ /// Computes a stable, deterministic 64-bit FNV-1a hash of and formats it as
+ /// 16 lowercase hex characters.
+ ///
+ /// A hand-rolled hash is required because the name must be identical across builds, machines and
+ /// target frameworks (string.GetHashCode() is randomized per process on modern .NET, and
+ /// System.HashCode is unavailable on netstandard2.0).
+ ///
+ ///
+ private static string Fnv1a64Hex(string value)
+ {
+ unchecked
+ {
+ const ulong offsetBasis = 14695981039346656037UL;
+ const ulong prime = 1099511628211UL;
+
+ var hash = offsetBasis;
+ foreach (var ch in value)
+ {
+ // Fold both bytes of each char so the hash is well-defined for any input, not just ASCII.
+ hash = (hash ^ (byte)ch) * prime;
+ hash = (hash ^ (byte)(ch >> 8)) * prime;
+ }
+
+ return hash.ToString("x16");
+ }
+ }
+}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
index 36d68bed..14c363c1 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
@@ -229,7 +229,17 @@ private static void Execute(
}
var generatedClassName = ProjectionExpressionClassNameGenerator.GenerateName(projectable.ClassNamespace, projectable.NestedInClassNames, projectable.MemberName, projectable.ParameterTypeNames);
- var generatedFileName = projectable.ClassTypeParameterList is not null ? $"{generatedClassName}-{projectable.ClassTypeParameterList.ChildNodes().Count()}.g.cs" : $"{generatedClassName}.g.cs";
+
+ // The generated class name embeds every parameter's fully-qualified type name and can grow to
+ // hundreds of characters. Using it verbatim as the file name overflows path limits (e.g. Windows
+ // MAX_PATH) and crashes Visual Studio when browsing generated files. The file (hint) name is never
+ // read at runtime, so it is shortened independently of the class name — which is a runtime contract
+ // (line below and the resolver/registry) and must stay unchanged. See GeneratedHintName.
+ var generatedFileBaseName = projectable.ClassTypeParameterList is not null
+ ? $"{generatedClassName}-{projectable.ClassTypeParameterList.ChildNodes().Count()}"
+ : generatedClassName;
+ var readableFileNamePrefix = ProjectionExpressionClassNameGenerator.GenerateName(projectable.ClassNamespace, projectable.NestedInClassNames, projectable.MemberName);
+ var generatedFileName = GeneratedHintName.Build(generatedFileBaseName, readableFileNamePrefix);
var classSyntax = ClassDeclaration(generatedClassName)
.WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword)))
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs
index cc5e1ed1..b1ebc381 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs
@@ -391,6 +391,72 @@ class C {
return Verifier.Verify(result.GeneratedTrees.Select(t => t.ToString()));
}
+ [Fact]
+ public void LongParameterMethod_HintNameIsBounded()
+ {
+ // A deeply-qualified nested generic parameter makes the generated class name — and, historically,
+ // the file name — exceed ~150 characters, which overflows path limits and crashes Visual Studio
+ // when browsing generated files. The hint name must be shortened well below that.
+ var compilation = CreateCompilation(@"
+using System.Collections.Generic;
+using EntityFrameworkCore.Projectables;
+namespace Some.Deep.Namespace.For.The.Test {
+ class Alpha {}
+ class Beta {}
+ class Container {
+ [Projectable]
+ public int Combine(Dictionary> values) => values.Count;
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ var hintName = result.GeneratedTrees[0].FilePath.Split('/', '\\')[^1];
+
+ // Bounded length (readable head + deterministic hash + ".g.cs") while staying recognizable via its
+ // namespace/member head. The un-shortened base name here is well over 150 characters.
+ Assert.EndsWith(".g.cs", hintName);
+ Assert.True(hintName.Length <= 69, $"Hint name too long ({hintName.Length}): {hintName}");
+ Assert.StartsWith("Some_Deep_", hintName);
+ }
+
+ [Fact]
+ public void LongOverloads_HintNamesAreUnique()
+ {
+ // Two overloads share the same class/member (identical readable prefix) but differ only in their
+ // long parameter types. The appended hash must keep the two hint names distinct, otherwise Roslyn
+ // rejects the duplicate hint name.
+ var compilation = CreateCompilation(@"
+using System.Collections.Generic;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ [Projectable]
+ public int Combine(Dictionary> values) => values.Count;
+
+ [Projectable]
+ public int Combine(Dictionary> values) => values.Count;
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ // Two trees (rather than a thrown duplicate-hint-name error) confirms the names disambiguated.
+ Assert.Equal(2, result.GeneratedTrees.Length);
+
+ var hintNames = result.GeneratedTrees.Select(t => t.FilePath.Split('/', '\\')[^1]).ToList();
+
+ Assert.Equal(2, hintNames.Distinct().Count());
+ Assert.All(hintNames, h => Assert.True(h.Length <= 69, $"Hint name too long ({h.Length}): {h}"));
+ Assert.All(hintNames, h => Assert.StartsWith("Foo_C_Combine_", h));
+ }
+
[Fact]
public Task InheritedMembers()
{