Skip to content

Commit 76466bf

Browse files
[C# Generator] Populate attributes from custom code properties and fields in NamedTypeSymbolProvider (#10322)
`NamedTypeSymbolProvider.BuildProperties()` and `BuildFields()` never passed symbol attributes to the `PropertyProvider`/`FieldProvider` constructors, so `CustomCodeView.Properties[*].Attributes` and `CustomCodeView.Fields[*].Attributes` were always empty. This blocked downstream consumers (e.g., mgmt generator) from detecting attributes like `[Obsolete]` on custom code properties and fields. ### Changes - **`NamedTypeSymbolProvider.cs`**: Extract `AttributeData` from each `IPropertySymbol` and `IFieldSymbol` and pass as `attributes:` parameter when constructing `PropertyProvider` and `FieldProvider`. All attributes (including internal CodeGen attributes) are passed through without filtering. Uses `.ToArray()` because the constructor's `(attributes as IReadOnlyList<AttributeStatement>) ?? []` silently drops unmaterialized `IEnumerable`. ```csharp new PropertyProvider( ..., this, attributes: propertySymbol.GetAttributes().Select(a => new AttributeStatement(a)).ToArray()) new FieldProvider( ..., attributes: fieldSymbol.GetAttributes().Select(a => new AttributeStatement(a)).ToArray()) ``` - **`FieldProvider.cs`**: Updated `InitializeParameter()` to filter out codegen-related attributes (from the `Microsoft.TypeSpec.Generator.Customizations` namespace) when propagating field attributes to constructor parameters via `AsParameter`. This ensures user-facing attributes like `[Obsolete]` propagate to generated constructor parameters, while internal codegen attributes like `[CodeGenMember]` do not appear in generated code. The field itself retains all attributes for downstream consumers. ```csharp var paramAttributes = Attributes.Where(a => a.Type.Namespace != CodeModelGenerator.CustomizationAttributeNamespace).ToArray(); _parameter = new(() => new ParameterProvider( ..., attributes: paramAttributes)); ``` - **Test**: Added `CanReadPropertyAttributes` with a custom code file declaring `[Obsolete]` (with a named argument `DiagnosticId`), `[EditorBrowsable]`, and a custom non-system attribute on a property, plus `[Obsolete]` on a field. The test validates attribute types using `CSharpType.Equals`, checks argument counts, validates `PositionalArguments`, and ensures the generator does not throw for custom non-system attributes. --------- 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 a894bbb commit 76466bf

4 files changed

Lines changed: 75 additions & 3 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics.CodeAnalysis;
7+
using System.Linq;
78
using Microsoft.TypeSpec.Generator.Expressions;
89
using Microsoft.TypeSpec.Generator.Input.Extensions;
910
using Microsoft.TypeSpec.Generator.Primitives;
@@ -147,8 +148,9 @@ public void Update(
147148
[MemberNotNull(nameof(_parameter))]
148149
private void InitializeParameter()
149150
{
151+
var paramAttributes = Attributes.Where(a => a.Type.Namespace != CodeModelGenerator.CustomizationAttributeNamespace).ToArray();
150152
_parameter = new(() => new ParameterProvider(
151-
Name.ToVariableName(), Description ?? FormattableStringHelpers.Empty, Type, field: this, wireInfo: WireInfo, attributes: Attributes));
153+
Name.ToVariableName(), Description ?? FormattableStringHelpers.Empty, Type, field: this, wireInfo: WireInfo, attributes: paramAttributes));
152154
}
153155

154156
private MemberExpression? _asMember;

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ protected internal override FieldProvider[] BuildFields()
122122
fieldSymbol.Name,
123123
this,
124124
GetSymbolXmlDoc(fieldSymbol, "summary"),
125-
initializationValue: GetFieldInitializer(fieldSymbol))
125+
initializationValue: GetFieldInitializer(fieldSymbol),
126+
attributes: fieldSymbol.GetAttributes().Select(a => new AttributeStatement(a)).ToArray())
126127
{
127128
OriginalName = GetOriginalName(fieldSymbol)
128129
};
@@ -146,7 +147,8 @@ protected internal override PropertyProvider[] BuildProperties()
146147
new AutoPropertyBody(
147148
propertySymbol.SetMethod is not null,
148149
InitializationExpression: GetPropertyInitializer(propertySymbol)),
149-
this)
150+
this,
151+
attributes: propertySymbol.GetAttributes().Select(a => new AttributeStatement(a)).ToArray())
150152
{
151153
OriginalName = GetOriginalName(propertySymbol),
152154
CustomProvider = new(() => propertySymbol.Type is INamedTypeSymbol propertyNamedTypeSymbol

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelCustomizationTests.cs

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

4+
using System;
45
using System.Collections.Generic;
56
using System.IO;
67
using System.Linq;
@@ -1698,6 +1699,50 @@ public async Task CanCustomizeBaseModelToSystemType()
16981699
"System.Exception is from a referenced assembly and should use SystemObjectTypeProvider");
16991700
}
17001701

1702+
[Test]
1703+
public async Task CanReadPropertyAttributes()
1704+
{
1705+
await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());
1706+
1707+
var props = new[]
1708+
{
1709+
InputFactory.Property("Prop1", InputPrimitiveType.String)
1710+
};
1711+
1712+
var inputModel = InputFactory.Model("mockInputModel", properties: props);
1713+
var modelTypeProvider = new ModelProvider(inputModel);
1714+
var customCodeView = modelTypeProvider.CustomCodeView;
1715+
1716+
Assert.IsNotNull(customCodeView);
1717+
Assert.AreEqual(1, customCodeView!.Properties.Count);
1718+
1719+
var customProperty = customCodeView.Properties[0];
1720+
Assert.AreEqual("Prop1", customProperty.Name);
1721+
1722+
// Verify that attributes from custom code are populated, including a custom non-system attribute
1723+
Assert.AreEqual(3, customProperty.Attributes.Count);
1724+
1725+
// Validate [Obsolete("This property is now deprecated.", DiagnosticId = "OBS001")] - type, arguments, and positional arguments
1726+
var obsoleteAttr = customProperty.Attributes.Single(a => new CSharpType(typeof(ObsoleteAttribute)).Equals(a.Type));
1727+
Assert.AreEqual(1, obsoleteAttr.Arguments.Count);
1728+
Assert.AreEqual(1, obsoleteAttr.PositionalArguments.Count);
1729+
Assert.AreEqual("DiagnosticId", obsoleteAttr.PositionalArguments[0].Key);
1730+
1731+
// Validate [EditorBrowsable(EditorBrowsableState.Never)] - type and arguments
1732+
var editorBrowsableAttr = customProperty.Attributes.Single(a => new CSharpType(typeof(System.ComponentModel.EditorBrowsableAttribute)).Equals(a.Type));
1733+
Assert.AreEqual(1, editorBrowsableAttr.Arguments.Count);
1734+
1735+
// Validate [Custom("custom message")] - custom non-system attribute does not throw
1736+
var customAttr = customProperty.Attributes.Single(a => a.Type.Name == "CustomAttribute");
1737+
Assert.AreEqual(1, customAttr.Arguments.Count);
1738+
1739+
// Verify that field attributes from custom code are populated
1740+
var customField = customCodeView.Fields.Single(f => f.Name == "_customField");
1741+
Assert.AreEqual(1, customField.Attributes.Count);
1742+
var fieldObsoleteAttr = customField.Attributes.Single(a => new CSharpType(typeof(ObsoleteAttribute)).Equals(a.Type));
1743+
Assert.AreEqual(1, fieldObsoleteAttr.Arguments.Count);
1744+
}
1745+
17011746
private class TestNameVisitor : NameVisitor
17021747
{
17031748
public TypeProvider? InvokeVisit(TypeProvider type)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#nullable disable
2+
3+
using System;
4+
using System.ComponentModel;
5+
6+
namespace Sample.Models
7+
{
8+
public class CustomAttribute : Attribute
9+
{
10+
public CustomAttribute(string message) { }
11+
}
12+
13+
public partial class MockInputModel
14+
{
15+
[Obsolete("This property is now deprecated.", DiagnosticId = "OBS001")]
16+
[EditorBrowsable(EditorBrowsableState.Never)]
17+
[Custom("custom message")]
18+
public string Prop1 { get; set; }
19+
20+
[Obsolete("This field is now deprecated.")]
21+
private int _customField;
22+
}
23+
}

0 commit comments

Comments
 (0)