Skip to content

Commit 59be7d7

Browse files
[http-client-csharp] Add CustomCodeAttributeDefinition base for custom-code attribute providers (#10997)
Custom-code attribute providers contributed via `CodeModelGenerator.AddCustomCodeAttributeProvider` are written into the custom-code workspace before `SourceInputModel` is initialized. For a normal `TypeProvider` outside the generator assembly, writing the provider touches the lazy `CustomCodeView`/`LastContractView`, which evaluate `SourceInputModel` and crash with *"SourceInputModel has not been initialized yet"*. Built-in providers avoid this by overriding `BuildCustomCodeView`/`BuildLastContractView` to return `null`, but those are `private protected` and unreachable from derived generators in other assemblies (e.g. Azure management), forcing reflection workarounds. ### Changes - **New base type `CustomCodeAttributeDefinition`** (`Microsoft.TypeSpec.Generator.Providers`): public abstract `TypeProvider` that `sealed override`s `BuildCustomCodeView`/`BuildLastContractView` to `null`. Living in the generator assembly lets it override the `private protected` members; derived generators inherit the opt-out with no reflection. - **Built-in definitions** (`CodeGenType`/`CodeGenMember`/`CodeGenSuppress`/`CodeGenSerialization`AttributeDefinition) now inherit from the base, dropping their duplicated per-class opt-outs. Generated output is unchanged. - **`CodeModelGenerator` APIs**: `CustomCodeAttributeProviders` is now `List<CustomCodeAttributeDefinition>` and `AddCustomCodeAttributeProvider` takes a `CustomCodeAttributeDefinition`, so contributed providers are required to inherit the base and the source-input opt-out is enforced at the type level rather than relying on the caller. - **Tests**: `TestCustomCodeAttributeDefinition` inherits the new base; added a test that sets `SourceInputModel` to throw and asserts a `CustomCodeAttributeDefinition`'s views/`Name`/`Type` resolve without throwing, while a plain `TypeProvider` still throws. ### Usage ```csharp public class CodeGenResourceDataAttributeDefinition : CustomCodeAttributeDefinition { protected override string BuildName() => "CodeGenResourceDataAttribute"; // BuildCustomCodeView / BuildLastContractView are sealed to null by the base } ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
1 parent ca236c8 commit 59be7d7

9 files changed

Lines changed: 56 additions & 24 deletions

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ internal set
100100
public IReadOnlyList<string> SharedSourceDirectories => _sharedSourceDirectories;
101101

102102
/// <summary>
103-
/// The list of <see cref="TypeProvider"/> instances that define custom-code attributes. These attribute
103+
/// The list of <see cref="CustomCodeAttributeDefinition"/> instances that define custom-code attributes. These attribute
104104
/// definitions are generated into the SDK project and are made available to the compiler while it compiles
105105
/// custom code. Derived generators can contribute additional providers via
106-
/// <see cref="AddCustomCodeAttributeProvider(TypeProvider)"/>.
106+
/// <see cref="AddCustomCodeAttributeProvider(CustomCodeAttributeDefinition)"/>.
107107
/// </summary>
108-
internal List<TypeProvider> CustomCodeAttributeProviders { get; } =
108+
internal List<CustomCodeAttributeDefinition> CustomCodeAttributeProviders { get; } =
109109
[
110110
new CodeGenTypeAttributeDefinition(),
111111
new CodeGenMemberAttributeDefinition(),
@@ -171,8 +171,8 @@ public virtual void AddSharedSourceDirectory(string sharedSourceDirectory)
171171
/// generator-specific attribute definitions. The provider's attribute definition is generated into
172172
/// the SDK project and made available to the compiler while it compiles custom code.
173173
/// </summary>
174-
/// <param name="provider">The <see cref="TypeProvider"/> that defines the custom-code attribute.</param>
175-
protected void AddCustomCodeAttributeProvider(TypeProvider provider)
174+
/// <param name="provider">The <see cref="CustomCodeAttributeDefinition"/> that defines the custom-code attribute.</param>
175+
protected void AddCustomCodeAttributeProvider(CustomCodeAttributeDefinition provider)
176176
{
177177
CustomCodeAttributeProviders.Add(provider);
178178
}

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CodeGenMemberAttributeDefinition.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@
1111

1212
namespace Microsoft.TypeSpec.Generator.Providers
1313
{
14-
internal class CodeGenMemberAttributeDefinition : TypeProvider
14+
internal class CodeGenMemberAttributeDefinition : CustomCodeAttributeDefinition
1515
{
1616
protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs");
1717

1818
protected override string BuildName() => "CodeGenMemberAttribute";
1919

2020
protected override string BuildNamespace() => CodeModelGenerator.CustomizationAttributeNamespace;
2121

22-
private protected sealed override NamedTypeSymbolProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
23-
private protected sealed override NamedTypeSymbolProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
24-
2522
protected override TypeSignatureModifiers BuildDeclarationModifiers() =>
2623
TypeSignatureModifiers.Internal | TypeSignatureModifiers.Class;
2724

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CodeGenSerializationAttributeDefinition.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@
1111

1212
namespace Microsoft.TypeSpec.Generator.Providers
1313
{
14-
internal class CodeGenSerializationAttributeDefinition : TypeProvider
14+
internal class CodeGenSerializationAttributeDefinition : CustomCodeAttributeDefinition
1515
{
1616
protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs");
1717

1818
protected override string BuildName() => "CodeGenSerializationAttribute";
1919

2020
protected override string BuildNamespace() => CodeModelGenerator.CustomizationAttributeNamespace;
2121

22-
private protected sealed override NamedTypeSymbolProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
23-
private protected sealed override NamedTypeSymbolProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
24-
2522
protected override TypeSignatureModifiers BuildDeclarationModifiers() =>
2623
TypeSignatureModifiers.Internal | TypeSignatureModifiers.Class;
2724

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CodeGenSuppressAttributeDefinition.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@
1111

1212
namespace Microsoft.TypeSpec.Generator.Providers
1313
{
14-
internal class CodeGenSuppressAttributeDefinition : TypeProvider
14+
internal class CodeGenSuppressAttributeDefinition : CustomCodeAttributeDefinition
1515
{
1616
protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs");
1717

1818
protected override string BuildName() => "CodeGenSuppressAttribute";
1919

2020
protected override string BuildNamespace() => CodeModelGenerator.CustomizationAttributeNamespace;
2121

22-
private protected sealed override NamedTypeSymbolProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
23-
private protected sealed override NamedTypeSymbolProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
24-
2522
protected override TypeSignatureModifiers BuildDeclarationModifiers() =>
2623
TypeSignatureModifiers.Internal | TypeSignatureModifiers.Class;
2724

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CodeGenTypeAttributeDefinition.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@
1111

1212
namespace Microsoft.TypeSpec.Generator.Providers
1313
{
14-
internal class CodeGenTypeAttributeDefinition : TypeProvider
14+
internal class CodeGenTypeAttributeDefinition : CustomCodeAttributeDefinition
1515
{
1616
protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs");
1717

1818
protected override string BuildName() => "CodeGenTypeAttribute";
1919

2020
protected override string BuildNamespace() => CodeModelGenerator.CustomizationAttributeNamespace;
2121

22-
private protected sealed override NamedTypeSymbolProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
23-
private protected sealed override NamedTypeSymbolProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
24-
2522
protected override TypeSignatureModifiers BuildDeclarationModifiers() =>
2623
TypeSignatureModifiers.Internal | TypeSignatureModifiers.Class;
2724

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.TypeSpec.Generator.Providers
5+
{
6+
/// <summary>
7+
/// A base type for custom-code attribute definitions that are generated into the SDK project and made available
8+
/// to the compiler while it compiles custom code.
9+
/// </summary>
10+
public abstract class CustomCodeAttributeDefinition : TypeProvider
11+
{
12+
private protected sealed override TypeProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
13+
private protected sealed override TypeProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
14+
}
15+
}

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratorTests.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using System;
45
using System.Linq;
6+
using Microsoft.TypeSpec.Generator.Providers;
7+
using Microsoft.TypeSpec.Generator.Tests.Common;
8+
using Moq;
59
using NUnit.Framework;
610

711
namespace Microsoft.TypeSpec.Generator.Tests
@@ -113,13 +117,38 @@ public void CanAddCustomCodeAttributeProvider()
113117
var mockGenerator = new TestGenerator();
114118
var initialCount = mockGenerator.CustomCodeAttributeProviders.Count;
115119

116-
var provider = new TestTypeProvider();
120+
var provider = new TestCustomCodeAttributeDefinition();
117121
mockGenerator.AddCustomCodeAttributeProviderForTest(provider);
118122

119123
Assert.AreEqual(initialCount + 1, mockGenerator.CustomCodeAttributeProviders.Count);
120124
Assert.AreSame(provider, mockGenerator.CustomCodeAttributeProviders[^1]);
121125
}
122126

127+
// Reproduces the real CSharpGen.ExecuteAsync ordering where contributed custom-code attribute providers are
128+
// written before SourceInputModel is initialized. A CustomCodeAttributeDefinition disables the source-input
129+
// views, so accessing its Name/Type (and the views directly) must not throw.
130+
[Test]
131+
public void CustomCodeAttributeDefinitionDoesNotEvaluateSourceInputViews()
132+
{
133+
var mockGenerator = MockHelpers.LoadMockGenerator();
134+
// Simulate SourceInputModel not being initialized yet, as is the case when the contributed attribute
135+
// provider is written into the custom-code workspace.
136+
mockGenerator
137+
.Setup(p => p.SourceInputModel)
138+
.Throws(new InvalidOperationException("SourceInputModel has not been initialized yet"));
139+
140+
var attributeDefinition = new TestCustomCodeAttributeDefinition();
141+
142+
Assert.IsNull(attributeDefinition.CustomCodeView);
143+
Assert.IsNull(attributeDefinition.LastContractView);
144+
Assert.DoesNotThrow(() => _ = attributeDefinition.Name);
145+
Assert.DoesNotThrow(() => _ = attributeDefinition.Type);
146+
147+
// A regular TypeProvider would still attempt to resolve the source-input views and therefore throw.
148+
var regularProvider = new TestTypeProvider();
149+
Assert.Throws<InvalidOperationException>(() => _ = regularProvider.CustomCodeView);
150+
}
151+
123152
private class DerivedTestLibraryVisitor : TestLibraryVisitor { }
124153
}
125154
}

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/TestCustomCodeAttributeDefinition.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace Microsoft.TypeSpec.Generator.Tests
1616
/// via <c>AddCustomCodeAttributeProvider</c>. Used to validate that custom code referencing the attribute is
1717
/// parsed and the attribute is registered on the parsed type.
1818
/// </summary>
19-
public class TestCustomCodeAttributeDefinition : TypeProvider
19+
public class TestCustomCodeAttributeDefinition : CustomCodeAttributeDefinition
2020
{
2121
public const string AttributeNamespace = "Sample.Customizations";
2222

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/TestGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public TestGenerator()
1212
{
1313
}
1414

15-
public void AddCustomCodeAttributeProviderForTest(TypeProvider provider)
15+
public void AddCustomCodeAttributeProviderForTest(CustomCodeAttributeDefinition provider)
1616
=> AddCustomCodeAttributeProvider(provider);
1717
}
1818
}

0 commit comments

Comments
 (0)