From 6fdceba39f3463d42ee33c69c152cdd81958fe23 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 18 Nov 2025 14:59:53 -0500 Subject: [PATCH 1/4] Updates for C# 14 reaching GA (#49940) * Remove "preview" LangVersion of preview shouldn't be needed anywhere now * Check for any "preview" langauge C# 14 is G.A. The `field` keyword is no longer a preview feature. Make those updates. * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * proofread and edit pass --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../dotnet-10/snippets/csharp/snippets.csproj | 1 - .../snippets/dotnet-9/csharp/Project.csproj | 1 - docs/csharp/includes/field-preview.md | 10 ---------- .../compiler-messages/partial-declarations.md | 10 ++++------ docs/csharp/language-reference/keywords/field.md | 6 ++---- docs/csharp/language-reference/keywords/get.md | 6 ++---- docs/csharp/language-reference/keywords/init.md | 6 ++---- docs/csharp/language-reference/keywords/set.md | 6 ++---- .../keywords/snippets/keywords.csproj | 1 - .../lambda-expressions/lambda-expressions.csproj | 3 +-- .../operators/snippets/shared/operators.csproj | 1 - .../statements/snippets/lock/lock.csproj | 3 +-- .../linq/snippets/HowToExtend/HowToExtend.csproj | 1 - .../auto-implemented-properties.md | 6 ++---- .../partial-classes-and-methods.md | 4 +--- .../classes-and-structs/properties.md | 6 ++---- .../ExtensionMembers/ExtensionMembers.csproj | 1 - .../PartialClassesAndMethods.csproj | 3 +-- .../snippets/properties/properties.csproj | 3 +-- .../classes-and-structs/using-properties.md | 10 ++++------ docs/csharp/snippets/methods/methods.csproj | 3 +-- docs/csharp/whats-new/csharp-13.md | 10 ++++++---- docs/csharp/whats-new/csharp-14.md | 16 +++++++--------- .../CompoundAssignment/CompoundAssignment.csproj | 3 +-- .../orleansurlshortener.csproj | 3 +-- 25 files changed, 41 insertions(+), 82 deletions(-) delete mode 100644 docs/csharp/includes/field-preview.md diff --git a/docs/core/whats-new/dotnet-10/snippets/csharp/snippets.csproj b/docs/core/whats-new/dotnet-10/snippets/csharp/snippets.csproj index 6f0d5c788bb02..a8aac614b7af1 100644 --- a/docs/core/whats-new/dotnet-10/snippets/csharp/snippets.csproj +++ b/docs/core/whats-new/dotnet-10/snippets/csharp/snippets.csproj @@ -5,7 +5,6 @@ Library enable enable - preview false diff --git a/docs/core/whats-new/snippets/dotnet-9/csharp/Project.csproj b/docs/core/whats-new/snippets/dotnet-9/csharp/Project.csproj index c48fa60e5ee07..ba96f2884f0ce 100644 --- a/docs/core/whats-new/snippets/dotnet-9/csharp/Project.csproj +++ b/docs/core/whats-new/snippets/dotnet-9/csharp/Project.csproj @@ -4,7 +4,6 @@ Exe net9 enable - preview diff --git a/docs/csharp/includes/field-preview.md b/docs/csharp/includes/field-preview.md deleted file mode 100644 index ba83ff9345bf8..0000000000000 --- a/docs/csharp/includes/field-preview.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -author: BillWagner -ms.author: wiwagn -ms.topic: include -ms.date: 10/30/2024 ---- - -> [!IMPORTANT] -> -> You should be careful using the `field` keyword feature in a class that has a field named `field`. The new `field` keyword shadows a field named `field` in the scope of a property accessor. You can either change the name of the `field` variable, or use the `@` token to reference the `field` identifier as `@field`. You can learn more by reading the feature specification for [the `field` keyword](~/_csharplang/proposals/csharp-14.0/field-keyword.md). diff --git a/docs/csharp/language-reference/compiler-messages/partial-declarations.md b/docs/csharp/language-reference/compiler-messages/partial-declarations.md index 9dbcfd384ebfd..5206ae63004aa 100644 --- a/docs/csharp/language-reference/compiler-messages/partial-declarations.md +++ b/docs/csharp/language-reference/compiler-messages/partial-declarations.md @@ -110,7 +110,7 @@ helpviewer_keywords: - "CS9278" - "CS9279" - "CS9280" -ms.date: 05/23/2025 +ms.date: 11/18/2025 --- # Errors and warnings related to `partial` type and `partial` member declarations @@ -293,7 +293,7 @@ public partial int ImplementingDeclaration { get => field; set; } - **CS9279**: *Partial event cannot have initializer.* - **CS9280**: *Only the implementing declaration of a partial constructor can have an initializer.* -You've declared an initializer on the defining declaration of a partial constructor or on a partial event declaration. You must remove it. +You declared an initializer on the defining declaration of a partial constructor or on a partial event declaration. You must remove it. ## field backed properties @@ -303,8 +303,6 @@ You've declared an initializer on the defining declaration of a partial construc - **CS9266**: *One accessor of property should use '`field`' because the other accessor is using it.* - **CS9273**: *In this language version, '`field`' is a keyword within a property accessor. Rename the variable or use the identifier '`@field`' instead.* -[!INCLUDE[field-preview](../../includes/field-preview.md)] +Beginning with C# 14, `field` backed properties allow you to access the compiler synthesized backing field for a property. **CS9258** or **CS9273** indicate that you have a variable named `field`, which can be hidden by the contextual keyword `field`. -Beginning with C# 13, the preview feature, `field` backed properties allows you to access the compiler synthesized backing field for a property. **CS9258** or **CS9273** indicate that you have a variable named `field`, which can be hidden by the contextual keyword `field`. - -**CS9263** indicates that your declaring declaration includes an implementation. That implementation might be accessing the compiler synthesized backing field for that property. **CS9264** indicates that the your use of `field` assumes a non-nullable backing field while the property declaration is nullable. The compiler assumes both the backing field and the property have the same nullability. You need to add the `[field:MaybeNull, AllowNull]` attribute to the property declaration to indicate that the `field` value should be considered nullable. **CS9266** indicates that one of a properties accessors uses the `field` keyword, but the other uses a hand-declared backing field. The warning indicates you may have done that by accident. +**CS9263** indicates that your declaring declaration includes an implementation. That implementation might be accessing the compiler synthesized backing field for that property. **CS9264** indicates that your use of `field` assumes a non-nullable backing field while the property declaration is nullable. The compiler assumes both the backing field and the property have the same nullability. You need to add the `[field:MaybeNull, AllowNull]` attribute to the property declaration to indicate that the `field` value should be considered nullable. **CS9266** indicates that one of a property's accessors uses the `field` keyword, but the other uses a hand-declared backing field. The warning indicates that might be a mistake. diff --git a/docs/csharp/language-reference/keywords/field.md b/docs/csharp/language-reference/keywords/field.md index 591f3368f1321..39c6f6accefde 100644 --- a/docs/csharp/language-reference/keywords/field.md +++ b/docs/csharp/language-reference/keywords/field.md @@ -1,7 +1,7 @@ --- description: "The `field` contextual keyword - access the compiler synthesized backing field for a property" title: "The `field` contextual keyword" -ms.date: 10/30/2024 +ms.date: 11/18/2025 f1_keywords: - "field_CSharpKeyword" helpviewer_keywords: @@ -9,9 +9,7 @@ helpviewer_keywords: --- # `field` - Field backed property declarations -[!INCLUDE[field-preview](../../includes/field-preview.md)] - -The contextual keyword `field`, added as a preview feature in C# 13, can be used in a property accessor to access the compiler synthesized backing field of a property. This syntax enables you to define the body of a `get` or `set` accessor and let the compiler generate the other accessor as it would in an automatically implemented property. +The contextual keyword `field`, added in C# 14, can be used in a property accessor to access the compiler synthesized backing field of a property. This syntax enables you to define the body of a `get` or `set` accessor and let the compiler generate the other accessor as it would in an automatically implemented property. The addition of the `field` contextual keywords provides a smooth path to add benefits such as range checking to an automatically implemented property. This practice is shown in the following example: diff --git a/docs/csharp/language-reference/keywords/get.md b/docs/csharp/language-reference/keywords/get.md index a2bb1724e2e0a..159f7ac1344c3 100644 --- a/docs/csharp/language-reference/keywords/get.md +++ b/docs/csharp/language-reference/keywords/get.md @@ -1,7 +1,7 @@ --- description: "The C# get keyword declares a get accessor in a property or indexer. It defines the code to retrieve the value of the property or indexed property." title: "The get keyword: property accessor" -ms.date: 10/30/2024 +ms.date: 11/18/2025 f1_keywords: - "get_CSharpKeyword" - "get" @@ -23,12 +23,10 @@ Often, the `get` accessor consists of a single statement that returns a value, a :::code language="csharp" source="./snippets/PropertyAccessors.cs" id="GetSetExpressions"::: -You might find that you need to implement one of the accessor bodies. You can use a field backed property to let the compiler generate one accessor while you write the other by hand. You use the `field` keyword, added as a preview feature in C# 13, to access the compiler synthesized backing field: +You might find that you need to implement one of the accessor bodies. You can use a field backed property to let the compiler generate one accessor while you write the other by hand. You use the `field` keyword, added in C# 14, to access the compiler synthesized backing field: :::code language="csharp" source="./snippets/PropertyAccessors.cs" id="FieldBackedProperty"::: -[!INCLUDE[field-preview](../../includes/field-preview.md)] - The following example defines both a `get` and a `set` accessor for a property named `Seconds`. It uses a private field named `_seconds` to back the property value. :::code language="csharp" source="./snippets/PropertyAccessors.cs" id="GetSetAccessors"::: diff --git a/docs/csharp/language-reference/keywords/init.md b/docs/csharp/language-reference/keywords/init.md index 75c18b1523893..13b3f5a9b8676 100644 --- a/docs/csharp/language-reference/keywords/init.md +++ b/docs/csharp/language-reference/keywords/init.md @@ -1,7 +1,7 @@ --- description: "The `init` keyword is used to declare a `set` accessor that can only be called during an object's initialization: either by a constructor or as part of an object initializer." title: "The init keyword - init only properties" -ms.date: 12/06/2023 +ms.date: 11/18/2025 f1_keywords: - "init" - "init_CSharpKeyword" @@ -17,12 +17,10 @@ The following code demonstrates an `init` accessor in an automatically implement :::code language="csharp" source="snippets/InitExample2.cs"::: -You might need to implement one of the accessors to provide parameter validation. You can do that using the `field` keyword, introduced as a preview feature in C# 13. The `field` keyword accesses the compiler synthesized backing field for that property. The following example shows a property where the `init` accessor validates the range of the `value` parameter" +You might need to implement one of the accessors to provide parameter validation. You can do that using the `field` keyword, introduced in C# 14. The `field` keyword accesses the compiler synthesized backing field for that property. The following example shows a property where the `init` accessor validates the range of the `value` parameter." :::code language="csharp" source="snippets/InitExample5.cs"::: -[!INCLUDE[field-preview](../../includes/field-preview.md)] - The `init` accessor can be used as an expression-bodied member. Example: :::code language="csharp" source="snippets/InitExample3.cs"::: diff --git a/docs/csharp/language-reference/keywords/set.md b/docs/csharp/language-reference/keywords/set.md index 811e2931856d7..aab81ecd64f0b 100644 --- a/docs/csharp/language-reference/keywords/set.md +++ b/docs/csharp/language-reference/keywords/set.md @@ -1,7 +1,7 @@ --- description: "The C# set keyword declares a set accessor in a property or indexer. It defines the code to set the value of the property or indexed property." title: "The `set` keyword: property accessor" -ms.date: 10/30/2024 +ms.date: 11/18/2025 f1_keywords: - "set" - "set_CSharpKeyword" @@ -19,12 +19,10 @@ For simple cases in which a property's `get` and `set` accessors perform no othe > [!IMPORTANT] > Automatically implemented properties aren't allowed for [interface property declarations](../../programming-guide/classes-and-structs/interface-properties.md) or the implementing declaration for a [partial property](./partial-member.md). The compiler interprets syntax matching an automatically implemented property as the declaring declaration, not an implementing declaration. -You might find that you need to implement one of the accessor bodies. The `field` keyword, added as a preview feature in C# 13 declares a field backed property. You can use a field backed property to let the compiler generate one accessor while you write the other by hand. You use the `field` keyword to access the compiler synthesized backing field: +You might find that you need to implement one of the accessor bodies. The `field` keyword, added in C# 14, declares a *field backed property*. You can use a field backed property to let the compiler generate one accessor while you write the other by hand. You use the `field` keyword to access the compiler synthesized backing field: :::code language="csharp" source="./snippets/PropertyAccessors.cs" id="FieldBackedProperty"::: -[!INCLUDE[field-preview](../../includes/field-preview.md)] - Often, the `set` accessor consists of a single statement that assigns a value, as it did in the previous example. You can implement the `set` accessor as an expression-bodied member. The following example implements both the `get` and the `set` accessors as expression-bodied members. :::code language="csharp" source="./snippets/PropertyAccessors.cs" id="GetSetExpressions"::: diff --git a/docs/csharp/language-reference/keywords/snippets/keywords.csproj b/docs/csharp/language-reference/keywords/snippets/keywords.csproj index e2d57b35b9e37..a0cd2fda66ab0 100644 --- a/docs/csharp/language-reference/keywords/snippets/keywords.csproj +++ b/docs/csharp/language-reference/keywords/snippets/keywords.csproj @@ -7,7 +7,6 @@ enable true Keywords.Program - preview False diff --git a/docs/csharp/language-reference/operators/snippets/lambda-expressions/lambda-expressions.csproj b/docs/csharp/language-reference/operators/snippets/lambda-expressions/lambda-expressions.csproj index 892cd8d092289..c28a5b02fb353 100644 --- a/docs/csharp/language-reference/operators/snippets/lambda-expressions/lambda-expressions.csproj +++ b/docs/csharp/language-reference/operators/snippets/lambda-expressions/lambda-expressions.csproj @@ -2,10 +2,9 @@ Exe - net9.0 + net10.0 enable enable - preview lambda_expressions lambda_expressions.Program diff --git a/docs/csharp/language-reference/operators/snippets/shared/operators.csproj b/docs/csharp/language-reference/operators/snippets/shared/operators.csproj index d3f3c672e90e0..f4d23c3a13581 100644 --- a/docs/csharp/language-reference/operators/snippets/shared/operators.csproj +++ b/docs/csharp/language-reference/operators/snippets/shared/operators.csproj @@ -6,7 +6,6 @@ enable true true - preview diff --git a/docs/csharp/language-reference/statements/snippets/lock/lock.csproj b/docs/csharp/language-reference/statements/snippets/lock/lock.csproj index 28787ee2b5764..ab51a5e3d3c4b 100644 --- a/docs/csharp/language-reference/statements/snippets/lock/lock.csproj +++ b/docs/csharp/language-reference/statements/snippets/lock/lock.csproj @@ -2,10 +2,9 @@ Exe - net9.0 + net10.0 enable enable - preview True diff --git a/docs/csharp/linq/snippets/HowToExtend/HowToExtend.csproj b/docs/csharp/linq/snippets/HowToExtend/HowToExtend.csproj index ff1b8f8e8e7cc..569283bafe29b 100644 --- a/docs/csharp/linq/snippets/HowToExtend/HowToExtend.csproj +++ b/docs/csharp/linq/snippets/HowToExtend/HowToExtend.csproj @@ -5,6 +5,5 @@ net10.0 enable enable - preview diff --git a/docs/csharp/programming-guide/classes-and-structs/auto-implemented-properties.md b/docs/csharp/programming-guide/classes-and-structs/auto-implemented-properties.md index 6ff1acc3facad..4c6fb3aef0c9c 100644 --- a/docs/csharp/programming-guide/classes-and-structs/auto-implemented-properties.md +++ b/docs/csharp/programming-guide/classes-and-structs/auto-implemented-properties.md @@ -1,7 +1,7 @@ --- title: "Automatically Implemented Properties" description: For an automatically implemented property in C#, the compiler creates a private, anonymous backing field accessed only through get and set accessors of the property. -ms.date: 10/30/2024 +ms.date: 11/18/2025 f1_keywords: - "propertyInitializer_CSharpKeyword" helpviewer_keywords: @@ -32,7 +32,7 @@ The class that is shown in the previous example is mutable. Client code can chan For more information, see [How to implement a lightweight class with automatically implemented properties](./how-to-implement-a-lightweight-class-with-auto-implemented-properties.md). -You might need to add validation to an automatically implemented property. C# 13 adds [field backed properties](../../language-reference/keywords/field.md) as a preview feature. You use the `field` keyword to access the compiler synthesized backing field of an automatically implemented property. For example, you could ensure that the `FirstName` property in the preceding example can't be set to `null` or the empty string: +You might need to add validation to an automatically implemented property. C# 14 adds [field backed properties](../../language-reference/keywords/field.md). You use the `field` keyword to access the compiler synthesized backing field of an automatically implemented property. For example, you could ensure that the `FirstName` property in the preceding example can't be set to `null` or the empty string: ```csharp public string FirstName @@ -49,8 +49,6 @@ public string FirstName This feature enables you to add logic to accessors without requiring you to explicitly declare the backing field. You use the `field` keyword to access the backing field generated by the compiler. -[!INCLUDE[field-preview](../../includes/field-preview.md)] - ## See also - [Use auto-implemented properties (style rule IDE0032)](../../../fundamentals/code-analysis/style-rules/ide0032.md) diff --git a/docs/csharp/programming-guide/classes-and-structs/partial-classes-and-methods.md b/docs/csharp/programming-guide/classes-and-structs/partial-classes-and-methods.md index 6645c4a758237..be18081c01c21 100644 --- a/docs/csharp/programming-guide/classes-and-structs/partial-classes-and-methods.md +++ b/docs/csharp/programming-guide/classes-and-structs/partial-classes-and-methods.md @@ -1,7 +1,7 @@ --- title: "Partial Classes and Members" description: Partial classes and members in C# split the definition of a class, a struct, an interface, or a member over two or more source files. -ms.date: 03/11/2025 +ms.date: 11/18/2025 helpviewer_keywords: - "partial methods [C#]" - "partial members [C#]" @@ -123,8 +123,6 @@ Beginning with C# 13, the implementing declaration for a partial property can us You can use `field` in either the `get` or `set` accessor, or both. -[!INCLUDE[field-preview](../../includes/field-preview.md)] - Partial members enable the implementer of one part of a class to declare a member. The implementer of another part of the class can define that member. There are two scenarios where this separation is useful: templates that generate boilerplate code, and source generators. - **Template code**: The template reserves a method name and signature so that generated code can call the method. These methods follow the restrictions that enable a developer to decide whether to implement the method. If the method isn't implemented, then the compiler removes the method signature and all calls to the method. The calls to the method, including any results that would occur from evaluation of arguments in the calls, have no effect at run time. Therefore, any code in the partial class can freely use a partial method, even if the implementation isn't supplied. No compile-time or run-time errors result if the method is called but not implemented. diff --git a/docs/csharp/programming-guide/classes-and-structs/properties.md b/docs/csharp/programming-guide/classes-and-structs/properties.md index 03ce87aa5ca30..cf0794e13a8a4 100644 --- a/docs/csharp/programming-guide/classes-and-structs/properties.md +++ b/docs/csharp/programming-guide/classes-and-structs/properties.md @@ -1,7 +1,7 @@ --- title: "Properties" description: A property in C# is a member that uses accessor methods to read, write, or compute the value of a private field as if it were a public data member. -ms.date: 10/30/2024 +ms.date: 11/18/2025 f1_keywords: - "cs.properties" helpviewer_keywords: @@ -28,12 +28,10 @@ You can initialize a property to a value other than the default by setting a val ## Field backed properties -In C# 13, you can add validation or other logic in the accessor for a property using the [`field`](../../language-reference/keywords/field.md) keyword preview feature. The `field` keyword accesses the compiler synthesized backing field for a property. It enables you to write a property accessor without explicitly declaring a separate backing field. +In C# 14, you can add validation or other logic in the accessor for a property using the [`field`](../../language-reference/keywords/field.md) keyword. The `field` keyword accesses the compiler synthesized backing field for a property. It enables you to write a property accessor without explicitly declaring a separate backing field. :::code language="csharp" source="./snippets/properties/Person.cs" id="FieldBackedProperty"::: -[!INCLUDE[field-preview](../../includes/field-preview.md)] - ## Required properties The preceding example allows a caller to create a `Person` using the default constructor, without setting the `FirstName` property. The property changed type to a *nullable* string. Beginning in C# 11, you can *require* callers to set a property: diff --git a/docs/csharp/programming-guide/classes-and-structs/snippets/ExtensionMembers/ExtensionMembers.csproj b/docs/csharp/programming-guide/classes-and-structs/snippets/ExtensionMembers/ExtensionMembers.csproj index 0b8f8f2bda02b..ed9781c223ab9 100644 --- a/docs/csharp/programming-guide/classes-and-structs/snippets/ExtensionMembers/ExtensionMembers.csproj +++ b/docs/csharp/programming-guide/classes-and-structs/snippets/ExtensionMembers/ExtensionMembers.csproj @@ -5,7 +5,6 @@ net10.0 enable enable - preview diff --git a/docs/csharp/programming-guide/classes-and-structs/snippets/partial-classes-and-methods/PartialClassesAndMethods.csproj b/docs/csharp/programming-guide/classes-and-structs/snippets/partial-classes-and-methods/PartialClassesAndMethods.csproj index 62f02481f1bf1..ad67af1752f5a 100644 --- a/docs/csharp/programming-guide/classes-and-structs/snippets/partial-classes-and-methods/PartialClassesAndMethods.csproj +++ b/docs/csharp/programming-guide/classes-and-structs/snippets/partial-classes-and-methods/PartialClassesAndMethods.csproj @@ -2,12 +2,11 @@ Exe - net9.0 + net10.0 enable enable PartialClassesAndMethods WrapCoords2.TestCoords - preview diff --git a/docs/csharp/programming-guide/classes-and-structs/snippets/properties/properties.csproj b/docs/csharp/programming-guide/classes-and-structs/snippets/properties/properties.csproj index a1bd0d0b2bb22..dfb40caafcf9a 100644 --- a/docs/csharp/programming-guide/classes-and-structs/snippets/properties/properties.csproj +++ b/docs/csharp/programming-guide/classes-and-structs/snippets/properties/properties.csproj @@ -2,10 +2,9 @@ Exe - net9.0 + net10.0 enable enable - preview diff --git a/docs/csharp/programming-guide/classes-and-structs/using-properties.md b/docs/csharp/programming-guide/classes-and-structs/using-properties.md index ec42866810316..7d903e2de1e4d 100644 --- a/docs/csharp/programming-guide/classes-and-structs/using-properties.md +++ b/docs/csharp/programming-guide/classes-and-structs/using-properties.md @@ -1,7 +1,7 @@ --- title: "Using Properties" description: These examples illustrate using properties in C#. See how the get and set accessors implement read and write access and find out about uses for properties. -ms.date: 10/31/2024 +ms.date: 11/18/2025 helpviewer_keywords: - "set accessor [C#]" - "get accessor [C#]" @@ -25,12 +25,10 @@ Properties are declared in the class block by specifying the access level of the In this example, `Month` is declared as a property so that the `set` accessor can make sure that the `Month` value is set between 1 and 12. The `Month` property uses a private field to track the actual value. The real location of a property's data is often referred to as the property's "backing store." It's common for properties to use private fields as a backing store. The field is marked private in order to make sure that it can only be changed by calling the property. For more information about public and private access restrictions, see [Access Modifiers](./access-modifiers.md). Automatically implemented properties provide simplified syntax for simple property declarations. For more information, see [Automatically implemented properties](auto-implemented-properties.md). -Beginning with C# 13, you can use [field backed properties](../../language-reference/keywords/field.md) to add validation to the `set` accessor of an automatically implemented property, as shown in the following example: +Beginning with C# 14, you can use [field backed properties](../../language-reference/keywords/field.md) to add validation to the `set` accessor of an automatically implemented property, as shown in the following example: :::code language="csharp" source="./snippets/properties/TimePeriod.cs" id="FieldExample"::: -[!INCLUDE[field-preview](../../includes/field-preview.md)] - ## The get accessor The body of the `get` accessor resembles that of a method. It must return a value of the property type. The C# compiler and Just-in-time (JIT) compiler detect common patterns for implementing the `get` accessor, and optimizes those patterns. For example, a `get` accessor that returns a field without performing any computation is likely optimized to a memory read of that field. Automatically implemented properties follow this pattern and benefit from these optimizations. However, a virtual `get` accessor method can't be inlined because the compiler doesn't know at compile time which method might actually be called at run time. The following example shows a `get` accessor that returns the value of a private field `_name`: @@ -80,7 +78,7 @@ A property can be marked as a virtual property by using the [virtual](../../lang A property overriding a virtual property can also be [sealed](../../language-reference/keywords/sealed.md), specifying that for derived classes it's no longer virtual. Lastly, a property can be declared [abstract](../../language-reference/keywords/abstract.md). Abstract properties don't define an implementation in the class, and derived classes must write their own implementation. For more information about these options, see [Abstract and Sealed Classes and Class Members](abstract-and-sealed-classes-and-class-members.md). > [!NOTE] -> It is an error to use a [virtual](../../language-reference/keywords/virtual.md), [abstract](../../language-reference/keywords/abstract.md), or [override](../../language-reference/keywords/override.md) modifier on an accessor of a [static](../../language-reference/keywords/static.md) property. +> It's an error to use a [virtual](../../language-reference/keywords/virtual.md), [abstract](../../language-reference/keywords/abstract.md), or [override](../../language-reference/keywords/override.md) modifier on an accessor of a [static](../../language-reference/keywords/static.md) property. ## Examples @@ -90,7 +88,7 @@ This example demonstrates instance, static, and read-only properties. It accepts ## Hidden property example -This example demonstrates how to access a property in a base class that is hidden by another property that has the same name in a derived class: +This example demonstrates how to access a property in a base class that's hidden by another property that has the same name in a derived class: :::code language="csharp" source="./snippets/Properties/HidingProperty.cs" id="Hiding"::: diff --git a/docs/csharp/snippets/methods/methods.csproj b/docs/csharp/snippets/methods/methods.csproj index 44828a6dc5714..686d49eabd76a 100644 --- a/docs/csharp/snippets/methods/methods.csproj +++ b/docs/csharp/snippets/methods/methods.csproj @@ -2,11 +2,10 @@ Exe - net9.0 + net10.0 enable enable methods.Program - preview diff --git a/docs/csharp/whats-new/csharp-13.md b/docs/csharp/whats-new/csharp-13.md index 48c3decac8069..384b763caf278 100644 --- a/docs/csharp/whats-new/csharp-13.md +++ b/docs/csharp/whats-new/csharp-13.md @@ -1,7 +1,7 @@ --- title: What's new in C# 13 description: Get an overview of the new features in C# 13. -ms.date: 05/29/2025 +ms.date: 11/18/2025 ms.topic: whats-new --- # What's new in C# 13 @@ -103,7 +103,7 @@ In versions before C# 13, the `^` operator can't be used in an object initialize ## `ref` and `unsafe` in iterators and `async` methods -This feature and the following two features enable `ref struct` types to use new constructs. You won't use these features unless you write your own `ref struct` types. More likely, you'll see an indirect benefit as and gain more functionality. +This feature and the following two features enable `ref struct` types to use new constructs. You won't use these features unless you write your own `ref struct` types. More likely, you see an indirect benefit as and gain more functionality. Before C# 13, iterator methods (methods that use `yield return`) and `async` methods couldn't declare local `ref` variables, nor could they have an `unsafe` context. @@ -140,7 +140,7 @@ Learn more in the updates on [`ref struct` types](../language-reference/builtin- ## More partial members -You can declare `partial` properties and `partial` indexers in C# 13. Partial properties and indexers generally follow the same rules as `partial` methods: you create one *declaring declaration* and one *implementing declaration*. The signatures of the two declarations must match. One restriction is that you can't use an auto-property declaration for *implementing* a partial property. Properties that don't declare a body are considered the *declaring declaration*. +You can declare `partial` properties and `partial` indexers in C# 13. Partial properties and indexers generally follow the same rules as `partial` methods: you create one *declaring declaration* and one *implementing declaration*. The signatures of the two declarations must match. One restriction is that you can't use an auto-property declaration for *implementing* a partial property. Properties that don't declare a body are considered as the *declaring declaration*. ```csharp public partial class C @@ -175,7 +175,9 @@ The [`field`](../language-reference/keywords/field.md) contextual keyword is in The `field` feature is released as a preview feature. We want to learn from your experiences using it. There's a potential breaking change or confusion reading code in types that also include a field named `field`. You can use `@field` or `this.field` to disambiguate between the `field` keyword and the identifier. -[!INCLUDE[field-preview](../includes/field-preview.md)] +> [!IMPORTANT] +> +> You should be careful using the `field` keyword feature in a class that has a field named `field`. The new `field` keyword shadows a field named `field` in the scope of a property accessor. You can either change the name of the `field` variable, or use the `@` token to reference the `field` identifier as `@field`. You can learn more by reading the feature specification for [the `field` keyword](~/_csharplang/proposals/csharp-14.0/field-keyword.md). If you try this feature and have feedback, add it to the [feature issue](https://github.com/dotnet/csharplang/issues/140) in the `csharplang` repository. diff --git a/docs/csharp/whats-new/csharp-14.md b/docs/csharp/whats-new/csharp-14.md index 0cd2fa5eb95ed..f982c1978c7b4 100644 --- a/docs/csharp/whats-new/csharp-14.md +++ b/docs/csharp/whats-new/csharp-14.md @@ -1,13 +1,13 @@ --- title: What's new in C# 14 description: Get an overview of the new features in C# 14. C# 14 ships with .NET 10. -ms.date: 09/17/2025 +ms.date: 11/18/2025 ms.topic: whats-new -ms.update-cycle: 180-days +ms.update-cycle: 365-days --- # What's new in C# 14 -C# 14 includes the following new features. You can try these features using the latest [Visual Studio 2022](https://visualstudio.microsoft.com/vs/preview/) version or the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet): +C# 14 includes the following new features. You can try these features using the latest [Visual Studio 2026](https://visualstudio.microsoft.com/) version or the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet): - [Extension members](#extension-members) - [Null-conditional assignment](#null-conditional-assignment) @@ -18,11 +18,13 @@ C# 14 includes the following new features. You can try these features using the - [`partial` events and constructors](#more-partial-members) - [user-defined compound assignment operators](#user-defined-compound-assignment) -C# 14 is supported on **.NET 10**. For more information, see [C# language versioning](../language-reference/configure-language-version.md). +C# 14 is the latest C# release. C# 14 is supported on **.NET 10**. For more information, see [C# language versioning](../language-reference/configure-language-version.md). -You can download the latest .NET 10 SDK from the [.NET downloads page](https://dotnet.microsoft.com/download). You can also download [Visual Studio 2022](https://visualstudio.microsoft.com/vs/), which includes the .NET 10 SDK. +You can download the latest .NET 10 SDK from the [.NET downloads page](https://dotnet.microsoft.com/download). You can also download [Visual Studio 2026](https://visualstudio.microsoft.com/vs/), which includes the .NET 10 SDK. + You can find any breaking changes introduced in C# 14 in our article on [breaking changes](~/_roslyn/docs/compilers/CSharp/Compiler%20Breaking%20Changes%20-%20DotNet%2010.md). @@ -93,10 +95,6 @@ You can declare a body for one or both accessors for a field backed property. There's a potential breaking change or confusion reading code in types that also include a symbol named `field`. You can use `@field` or `this.field` to disambiguate between the `field` keyword and the identifier, or you can rename the current `field` symbol to provide better distinction. -If you try this feature and have feedback, comment on the [feature issue](https://github.com/dotnet/csharplang/issues/140) in the `csharplang` repository. - -The [`field`](../language-reference/keywords/field.md) contextual keyword is in C# 13 as a preview feature. - ## Implicit span conversions C# 14 introduces first-class support for and in the language. This support involves new implicit conversions allowing more natural programming with these types. diff --git a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/CompoundAssignment.csproj b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/CompoundAssignment.csproj index b9dbadc899514..ec12a9ea60c06 100644 --- a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/CompoundAssignment.csproj +++ b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/CompoundAssignment.csproj @@ -5,7 +5,6 @@ net10.0 enable enable - preview - + diff --git a/docs/orleans/quickstarts/snippets/url-shortener/orleansurlshortener/orleansurlshortener.csproj b/docs/orleans/quickstarts/snippets/url-shortener/orleansurlshortener/orleansurlshortener.csproj index 2bc0b009412cf..ce3600f8a54da 100644 --- a/docs/orleans/quickstarts/snippets/url-shortener/orleansurlshortener/orleansurlshortener.csproj +++ b/docs/orleans/quickstarts/snippets/url-shortener/orleansurlshortener/orleansurlshortener.csproj @@ -1,10 +1,9 @@  - net8.0 + net10.0 disable enable - preview From 484d8f074bb1b68981a64e435db740d31a4c1fee Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:34:42 -0800 Subject: [PATCH 2/4] Add article about accessing data in AI functions (#49944) --- docs/ai/how-to/access-data-in-functions.md | 57 ++++++++++++++ .../snippets/access-data/ArgumentsExample.cs | 78 +++++++++++++++++++ .../ai/how-to/snippets/access-data/Program.cs | 2 + .../snippets/access-data/Project.csproj | 21 +++++ .../quickstarts/includes/create-ai-service.md | 1 + .../azure-openai/FunctionCallingAI.csproj | 4 +- .../function-calling/azure-openai/Program.cs | 37 +++++---- .../function-calling/openai/Program.cs | 2 +- docs/ai/quickstarts/use-function-calling.md | 12 +-- docs/ai/toc.yml | 18 +++-- 10 files changed, 198 insertions(+), 34 deletions(-) create mode 100644 docs/ai/how-to/access-data-in-functions.md create mode 100644 docs/ai/how-to/snippets/access-data/ArgumentsExample.cs create mode 100644 docs/ai/how-to/snippets/access-data/Program.cs create mode 100644 docs/ai/how-to/snippets/access-data/Project.csproj diff --git a/docs/ai/how-to/access-data-in-functions.md b/docs/ai/how-to/access-data-in-functions.md new file mode 100644 index 0000000000000..291b974d6a2f5 --- /dev/null +++ b/docs/ai/how-to/access-data-in-functions.md @@ -0,0 +1,57 @@ +--- +title: Access data in AI functions +description: Learn how to pass data to AIFunction objects and how to access the data within the function delegate. +ms.date: 11/17/2025 +--- + +# Access data in AI functions + +When you create AI functions, you might need to access contextual data beyond the parameters provided by the AI model. The `Microsoft.Extensions.AI` library provides several mechanisms to pass data to function delegates. + +## `AIFunction` class + +The type represents a function that can be described to an AI service and invoked. You can create `AIFunction` objects by calling one of the overloads. But is also a base class, and you can derive from it and implement your own AI function type. provides an easy way to wrap an existing `AIFunction` and layer in additional functionality, including capturing additional data to be used. + +## Pass data + +You can associate data with the function at the time it's created, either via closure or via . If you're creating your own function, you can populate `AdditionalProperties` however you want. If you use to create the function, you can populate data using . + +You can also capture any references to data as part of the delegate provided to `AIFunctionFactory`. That is, you can bake in whatever you want to reference as part of the `AIFunction` itself. + +## Access data in function delegates + +You might call your `AIFunction` directly, or you might call it indirectly by using . The following sections describe how to access argument data using either approach. + +### Manual function invocation + +If you manually invoke an by calling , you pass in . The type includes: + +- A dictionary of named arguments. +- : An arbitrary `IDictionary` for passing additional ambient data into the function. +- : An that lets the `AIFunction` resolve arbitrary state from a [dependency injection (DI)](../../core/extensions/dependency-injection.md) container. + +If you want to access either the `AIFunctionArguments` or the `IServiceProvider` from within your delegate, create a parameter typed as `IServiceProvider` or `AIFunctionArguments`. That parameter will be bound to the relevant data from the `AIFunctionArguments` passed to `AIFunction.InvokeAsync()`. + +The following code shows an example: + +:::code language="csharp" source="snippets/access-data/ArgumentsExample.cs" id="UseAIFunctionArguments"::: + + is also special-cased: if the `AIFunctionFactory.Create` delegate or lambda has a `CancellationToken` parameter, it will be bound to the `CancellationToken` that was passed to `AIFunction.InvokeAsync()`. + +### Invocation through `FunctionInvokingChatClient` + + publishes state about the current invocation to , including not only the arguments, but all of the input `ChatMessage` objects, the , and details on which function is being invoked (out of how many). You can add any data you want into and extract that inside of your `AIFunction` from `FunctionInvokingChatClient.CurrentContext.Options.AdditionalProperties`. + +The following code shows an example: + +:::code language="csharp" source="snippets/access-data/ArgumentsExample.cs" id="UseAdditionalProperties"::: + +#### Dependency injection + +If you use to invoke functions automatically, that client configures an object that it passes into the `AIFunction`. Because `AIFunctionArguments` includes the `IServiceProvider` that the `FunctionInvokingChatClient` was itself provided with, if you construct your client using standard DI means, that `IServiceProvider` is passed all the way into your `AIFunction`. At that point, you can query it for anything you want from DI. + +## Advanced techniques + +If you want more fine-grained control over how parameters are bound, you can use , which puts you in control over how each parameter is populated. For example, the [MCP C# SDK uses this technique](https://github.com/modelcontextprotocol/csharp-sdk/blob/d344c651203841ec1c9e828736d234a6e4aebd07/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs#L83-L107) to automatically bind parameters from DI. + +If you use the overload, you can also run your own arbitrary logic when you create the target object that the instance method will be called on, each time. And you can do whatever you want to configure that instance. diff --git a/docs/ai/how-to/snippets/access-data/ArgumentsExample.cs b/docs/ai/how-to/snippets/access-data/ArgumentsExample.cs new file mode 100644 index 0000000000000..969d18fc26814 --- /dev/null +++ b/docs/ai/how-to/snippets/access-data/ArgumentsExample.cs @@ -0,0 +1,78 @@ +// +using Azure; +using Azure.AI.OpenAI; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; + +class ArgumentsExample +{ + public static async Task RunManual() + { + // + Delegate getWeatherDelegate = (AIFunctionArguments args) => + { + // Access named parameters from the arguments dictionary. + string? location = args.TryGetValue("location", out object? loc) ? loc.ToString() : "Unknown"; + string? units = args.TryGetValue("units", out object? u) ? u.ToString() : "celsius"; + + return $"Weather in {location}: 35°{units}"; + }; + + // Create the AIFunction. + AIFunction getWeather = AIFunctionFactory.Create(getWeatherDelegate); + + // Call the function manually. + var result = await getWeather.InvokeAsync(new AIFunctionArguments + { + { "location", "Seattle" }, + { "units", "F" } + }); + Console.WriteLine($"Function result: {result}"); + // + } + public static async Task UseFICC() + { + IConfigurationRoot config = new ConfigurationBuilder() + .AddUserSecrets() + .Build(); + + string endpoint = config["AZURE_OPENAI_ENDPOINT"]; + string apiKey = config["AZURE_OPENAI_API_KEY"]; + string model = config["AZURE_OPENAI_GPT_NAME"]; + + // + FunctionInvokingChatClient client = new FunctionInvokingChatClient( + new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey)) + .GetChatClient(model).AsIChatClient()); + + AIFunction getWeather = AIFunctionFactory.Create(() => + { + // Access named parameters from the arguments dictionary. + AdditionalPropertiesDictionary props = + FunctionInvokingChatClient.CurrentContext.Options.AdditionalProperties; + + string location = props["location"].ToString(); + string units = props["units"].ToString(); + + return $"Weather in {location}: 35°{units}"; + }); + + var chatOptions = new ChatOptions + { + Tools = [getWeather], + AdditionalProperties = new AdditionalPropertiesDictionary { + ["location"] = "Seattle", + ["units"] = "F" + }, + }; + + List chatHistory = [ + new(ChatRole.System, "You're a helpful weather assistant.") + ]; + chatHistory.Add(new ChatMessage(ChatRole.User, "What's the weather like?")); + + ChatResponse response = await client.GetResponseAsync(chatHistory, chatOptions); + Console.WriteLine($"Response: {response.Text}"); + // + } +} diff --git a/docs/ai/how-to/snippets/access-data/Program.cs b/docs/ai/how-to/snippets/access-data/Program.cs new file mode 100644 index 0000000000000..b475775d3536b --- /dev/null +++ b/docs/ai/how-to/snippets/access-data/Program.cs @@ -0,0 +1,2 @@ +//await ArgumentsExample.RunManual(); +await ArgumentsExample.UseFICC(); diff --git a/docs/ai/how-to/snippets/access-data/Project.csproj b/docs/ai/how-to/snippets/access-data/Project.csproj new file mode 100644 index 0000000000000..8dc5dffa5c556 --- /dev/null +++ b/docs/ai/how-to/snippets/access-data/Project.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + enable + enable + 4d162886-0da5-4b62-a4db-d09780d06911 + + + + + + + + + + + + + diff --git a/docs/ai/quickstarts/includes/create-ai-service.md b/docs/ai/quickstarts/includes/create-ai-service.md index d000ddb2f3c88..0290feaddcd67 100644 --- a/docs/ai/quickstarts/includes/create-ai-service.md +++ b/docs/ai/quickstarts/includes/create-ai-service.md @@ -10,4 +10,5 @@ dotnet user-secrets init dotnet user-secrets set AZURE_OPENAI_ENDPOINT dotnet user-secrets set AZURE_OPENAI_GPT_NAME + dotnet user-secrets set AZURE_OPENAI_API_KEY ``` diff --git a/docs/ai/quickstarts/snippets/function-calling/azure-openai/FunctionCallingAI.csproj b/docs/ai/quickstarts/snippets/function-calling/azure-openai/FunctionCallingAI.csproj index e4747a83d5ed9..8dc5dffa5c556 100644 --- a/docs/ai/quickstarts/snippets/function-calling/azure-openai/FunctionCallingAI.csproj +++ b/docs/ai/quickstarts/snippets/function-calling/azure-openai/FunctionCallingAI.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable 4d162886-0da5-4b62-a4db-d09780d06911 @@ -11,7 +11,7 @@ - + diff --git a/docs/ai/quickstarts/snippets/function-calling/azure-openai/Program.cs b/docs/ai/quickstarts/snippets/function-calling/azure-openai/Program.cs index a8d3a53750ed0..3bf804265772a 100644 --- a/docs/ai/quickstarts/snippets/function-calling/azure-openai/Program.cs +++ b/docs/ai/quickstarts/snippets/function-calling/azure-openai/Program.cs @@ -1,39 +1,42 @@ -// -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.AI; +using Azure; using Azure.AI.OpenAI; -using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; -var config = new ConfigurationBuilder().AddUserSecrets().Build(); +// +IConfigurationRoot config = new ConfigurationBuilder().AddUserSecrets().Build(); string endpoint = config["AZURE_OPENAI_ENDPOINT"]; string deployment = config["AZURE_OPENAI_GPT_NAME"]; +string apiKey = config["AZURE_OPENAI_API_KEY"]; IChatClient client = new ChatClientBuilder( - new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) + new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey)) .GetChatClient(deployment).AsIChatClient()) .UseFunctionInvocation() .Build(); // -// Add a new plugin with a local .NET function that should be available to the AI model +// Add a new plugin with a local .NET function +// that should be available to the AI model. var chatOptions = new ChatOptions { Tools = [AIFunctionFactory.Create((string location, string unit) => - { - // Here you would call a weather API to get the weather for the location - return "Periods of rain or drizzle, 15 C"; - }, - "get_current_weather", - "Get the current weather in a given location")] + { + // Here you would call a weather API to + // get the weather for the location. + return "Periods of rain or drizzle, 15 C"; + }, + "get_current_weather", + "Get the current weather in a given location")] }; -// System prompt to provide context +// System prompt to provide context. List chatHistory = [new(ChatRole.System, """ - You are a hiking enthusiast who helps people discover fun hikes in their area. You are upbeat and friendly. - """)]; + You are a hiking enthusiast who helps people discover fun hikes in their area. You are upbeat and friendly. + """)]; -// Weather conversation relevant to the registered function +// Weather conversation relevant to the registered function. chatHistory.Add(new ChatMessage(ChatRole.User, "I live in Montreal and I'm looking for a moderate intensity hike. What's the current weather like? ")); Console.WriteLine($"{chatHistory.Last().Role} >>> {chatHistory.Last()}"); diff --git a/docs/ai/quickstarts/snippets/function-calling/openai/Program.cs b/docs/ai/quickstarts/snippets/function-calling/openai/Program.cs index d515895d5c11c..5238a08618d6d 100644 --- a/docs/ai/quickstarts/snippets/function-calling/openai/Program.cs +++ b/docs/ai/quickstarts/snippets/function-calling/openai/Program.cs @@ -25,7 +25,7 @@ return "Periods of rain or drizzle, 15 C"; }, "get_current_weather", - "Get the current weather in a given location")] + "Gets the current weather in a given location")] }; // diff --git a/docs/ai/quickstarts/use-function-calling.md b/docs/ai/quickstarts/use-function-calling.md index ca51d59d16a31..b6b700c2241b9 100644 --- a/docs/ai/quickstarts/use-function-calling.md +++ b/docs/ai/quickstarts/use-function-calling.md @@ -1,7 +1,7 @@ --- title: Quickstart - Extend OpenAI using Tools and execute a local Function with .NET description: Create a simple chat app using OpenAI and extend the model to execute a local function. -ms.date: 03/13/2025 +ms.date: 11/13/2025 ms.topic: quickstart zone_pivot_groups: openai-library # CustomerIntent: As a .NET developer new to OpenAI, I want deploy and use sample code to interact to learn from the sample code how to extend the model using Tools. @@ -9,7 +9,7 @@ zone_pivot_groups: openai-library # Invoke .NET functions using an AI model -In this quickstart, you create a .NET console AI chat app to connect to an AI model with local function calling enabled. The app uses the library so you can write code using AI abstractions rather than a specific SDK. AI abstractions enable you to change the underlying AI model with minimal code changes. +In this quickstart, you create a .NET console AI chat app that connects to an AI model with local function calling enabled. The app uses the library so you can write code using AI abstractions rather than a specific SDK. AI abstractions enable you to change the underlying AI model with minimal code changes. :::zone target="docs" pivot="openai" @@ -23,8 +23,6 @@ In this quickstart, you create a .NET console AI chat app to connect to an AI mo :::zone-end -[!INCLUDE [semantic-kernel](includes/semantic-kernel.md)] - ## Create the app Complete the following steps to create a .NET console app to connect to an AI model. @@ -103,10 +101,7 @@ The app uses the [`Microsoft.Extensions.AI`](https://www.nuget.org/packages/Micr :::zone target="docs" pivot="azure-openai" - :::code language="csharp" source="snippets/function-calling/azure-openai/program.cs" id="GetChatClient"::: - - > [!NOTE] - > searches for authentication credentials from your local tooling. If you aren't using the `azd` template to provision the Azure OpenAI resource, you'll need to assign the `Azure AI Developer` role to the account you used to sign in to Visual Studio or the Azure CLI. For more information, see [Authenticate to Azure AI services with .NET](../azure-ai-services-authentication.md). + :::code language="csharp" source="snippets/function-calling/azure-openai/Program.cs" id="GetChatClient"::: :::zone-end @@ -145,5 +140,6 @@ If you no longer need them, delete the Azure OpenAI resource and GPT-4 model dep ## Next steps +- [Access data in AI functions](../how-to/access-data-in-functions.md) - [Quickstart - Build an AI chat app with .NET](build-chat-app.md) - [Generate text and conversations with .NET and Azure OpenAI Completions](/training/modules/open-ai-dotnet-text-completions/) diff --git a/docs/ai/toc.yml b/docs/ai/toc.yml index bded8c4e59964..b49af499ac0ef 100644 --- a/docs/ai/toc.yml +++ b/docs/ai/toc.yml @@ -54,6 +54,18 @@ items: href: conceptual/rag.md - name: OpenAI function calling href: conceptual/understanding-openai-functions.md +- name: Call functions + items: + - name: "Quickstart: Execute a local function" + href: quickstarts/use-function-calling.md + - name: Access data in AI functions + href: how-to/access-data-in-functions.md +- name: Text to image + items: + - name: Generate images using MEAI + href: quickstarts/text-to-image.md + - name: Generate images using OpenAI.Images.ImageClient + href: quickstarts/generate-images.md - name: Chat with your data (RAG) items: - name: Get started with the RAG sample @@ -68,12 +80,6 @@ items: href: quickstarts/build-mcp-server.md - name: Build a minimal MCP client href: quickstarts/build-mcp-client.md -- name: Text to image - items: - - name: Generate images using MEAI - href: quickstarts/text-to-image.md - - name: Generate images using OpenAI.Images.ImageClient - href: quickstarts/generate-images.md - name: Security and content safety items: - name: Authentication for Azure-hosted apps and services From 5aa08ccf9eb8d89860d21a4ccb3e12212e19d8f9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:38:04 -0800 Subject: [PATCH 3/4] Refactor trimming docs for clarity (#49698) * Initial plan * Refactor trimming docs for clarity * Initial plan * Remove serialization and DI examples from trimming docs Removed examples for serialization and dependency injection patterns from trimming concepts documentation. * Add generic type constraints analogy to DAMT section Co-authored-by: agocke <515774+agocke@users.noreply.github.com> * Fix linting failures - add blank lines before lists Co-authored-by: agocke <515774+agocke@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> * Update docs/core/deploying/trimming/fixing-warnings.md Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> * Address feedback: imperative mood and ordered list Co-authored-by: agocke <515774+agocke@users.noreply.github.com> * Add anchor for functionality-with-requirements-on-its-input Co-authored-by: agocke <515774+agocke@users.noreply.github.com> * Fix anchor syntax and table formatting Co-authored-by: agocke <515774+agocke@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Andy Gocke Co-authored-by: Andy Gocke Co-authored-by: agocke <515774+agocke@users.noreply.github.com> Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> --- .../deploying/trimming/fixing-warnings.md | 461 ++++++++++++------ .../deploying/trimming/trim-self-contained.md | 18 +- .../deploying/trimming/trimming-concepts.md | 402 +++++++++++++++ docs/navigate/devops-testing/toc.yml | 32 +- 4 files changed, 744 insertions(+), 169 deletions(-) create mode 100644 docs/core/deploying/trimming/trimming-concepts.md diff --git a/docs/core/deploying/trimming/fixing-warnings.md b/docs/core/deploying/trimming/fixing-warnings.md index 26d9574e14716..9667f1a31afa1 100644 --- a/docs/core/deploying/trimming/fixing-warnings.md +++ b/docs/core/deploying/trimming/fixing-warnings.md @@ -1,276 +1,431 @@ --- -title: Introduction to trim warnings -description: Learn about why warnings might be produced when publishing a trimmed application, how to address them, and how to make the application "trim compatible." +title: Fixing trim warnings +description: Learn practical workflows for addressing trim warnings in your application, including step-by-step instructions for making code trim-compatible. author: agocke ms.author: angocke -ms.date: 10/30/2023 +ms.date: 11/04/2025 --- -# Introduction to trim warnings +# Fixing trim warnings -Conceptually, [trimming](trim-self-contained.md) is simple: when you publish an application, the .NET SDK analyzes the entire application and removes all unused code. However, it can be difficult to determine what is unused, or more precisely, what is used. +When you enable trimming in your application, the .NET SDK performs static analysis to detect code patterns that might not be compatible with trimming. Trim warnings indicate potential issues that could cause behavior changes or crashes after trimming. -To prevent changes in behavior when trimming applications, the .NET SDK provides static analysis of trim compatibility through **trim warnings**. The trimmer produces trim warnings when it finds code that might not be compatible with trimming. Code that's not trim-compatible can produce behavioral changes, or even crashes, in an application after it has been trimmed. An app that uses trimming shouldn't produce any trim warnings. If there are any trim warnings, the app should be thoroughly tested after trimming to ensure that there are no behavior changes. +**An app that uses trimming shouldn't produce any trim warnings.** If there are any trim warnings, thoroughly test the app after trimming to ensure there are no behavior changes. -This article helps you understand why some patterns produce trim warnings, and how these warnings can be addressed. +This article provides practical workflows for addressing trim warnings. For a deeper understanding of why these warnings occur and how trimming works, see [Understanding trim analysis](trimming-concepts.md). -## Examples of trim warnings +## Understanding warning categories -For most C# code, it's straightforward to determine what code is used and what code is unused—the trimmer can walk method calls, field and property references, and so on, and determine what code is accessed. Unfortunately, some features, like reflection, present a significant problem. Consider the following code: +Trim warnings fall into two main categories: -```csharp -string s = Console.ReadLine(); -Type type = Type.GetType(s); -foreach (var m in type.GetMethods()) -{ - Console.WriteLine(m.Name); -} -``` +- **Code incompatible with trimming** - Marked with . The code fundamentally can't be made analyzable (for example, dynamic assembly loading or complex reflection patterns). The method is marked as incompatible, and callers receive warnings. -In this example, dynamically requests a type with an unknown name, and then prints the names of all of its methods. Because there's no way to know at publish-time what type name is going to be used, there's no way for the trimmer to know which type to preserve in the output. It's likely that this code could have worked before trimming (as long as the input is something known to exist in the target framework), but would probably produce a null reference exception after trimming, as `Type.GetType` returns null when the type isn't found. +- **Code with requirements** - Annotated with . Reflection is used, but types are known at compile time. When requirements are satisfied, the code becomes fully trim-compatible. -In this case, the trimmer issues a warning on the call to `Type.GetType`, indicating that it can't determine which type is going to be used by the application. +## Workflow: Determine the right approach -## Reacting to trim warnings +When you encounter a trim warning, follow these steps in order: -Trim warnings are meant to bring predictability to trimming. There are two large categories of warnings that you'll likely see: +1. **Eliminate reflection** - This is always the best option if possible. +2. **Use DynamicallyAccessedMembers** - If types are known, make the code trim-compatible. +3. **Use RequiresUnreferencedCode** - If truly dynamic, document the incompatibility. +4. **Suppress warnings as last resort** - Only if you're certain the code is safe. -1. Functionality isn't compatible with trimming -2. Functionality has certain requirements on the input to be trim compatible +## Approach 1: Eliminate reflection -### Functionality incompatible with trimming +The best solution is to avoid reflection entirely when possible. This makes your code faster and fully trim-compatible. -These are typically methods that either don't work at all, or might be broken in some cases if they're used in a trimmed application. A good example is the `Type.GetType` method from the previous example. In a trimmed app it might work, but there's no guarantee. Such APIs are marked with . +### Use compile-time generics - is simple and broad: it's an attribute that means the member has been annotated incompatible with trimming. This attribute is used when code is fundamentally not trim compatible, or the trim dependency is too complex to explain to the trimmer. This would often be true for methods that dynamically load code for example via , enumerate or search through all types in an application or assembly for example via , use the C# [`dynamic`](../../../csharp/language-reference/builtin-types/reference-types.md#the-dynamic-type) keyword, or use other runtime code generation technologies. An example would be: +Replace runtime type operations with compile-time generic parameters: ```csharp -[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead")] -void MethodWithAssemblyLoad() +// ❌ Before: Uses reflection +void CreateAndProcess(Type type) { - ... - Assembly.LoadFrom(...); - ... + var instance = Activator.CreateInstance(type); + // Process instance... } -void TestMethod() +// ✅ After: Uses generics +void CreateAndProcess() where T : new() { - // IL2026: Using method 'MethodWithAssemblyLoad' which has 'RequiresUnreferencedCodeAttribute' - // can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead. - MethodWithAssemblyLoad(); + var instance = new T(); + // Process instance... } ``` -There aren't many workarounds for `RequiresUnreferencedCode`. The best fix is to avoid calling the method at all when trimming and use something else that's trim-compatible. +### Use source generators -#### Mark functionality as incompatible with trimming +Modern .NET provides source generators for common reflection scenarios: -If you're writing a library and it's not in your control whether or not to use incompatible functionality, you can mark it with `RequiresUnreferencedCode`. This annotates your method as incompatible with trimming. Using `RequiresUnreferencedCode` silences all trim warnings in the given method, but produces a warning whenever someone else calls it. +- **Serialization**: Use [System.Text.Json source generation](../../../standard/serialization/system-text-json/source-generation.md) instead of reflection-based serializers +- **Configuration**: Use the [configuration binding source generator](../../whats-new/dotnet-8/runtime.md#configuration-binding-source-generator) -The requires you to specify a `Message`. The message is shown as part of a warning reported to the developer who calls the marked method. For example: +For more information, see [Known trimming incompatibilities](incompatibilities.md). -```console -IL2026: Using member which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. -``` + -With the example above, a warning for a specific method might look like this: +## Approach 2: Make code trim-compatible with DynamicallyAccessedMembers -```console -IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead. -``` - -Developers calling such APIs are generally not going to be interested in the particulars of the affected API or specifics as it relates to trimming. +When reflection is necessary but types are known at compile time, use to make your code trim-compatible. -A good message should state what functionality isn't compatible with trimming and then guide the developer what are their potential next steps. It might suggest to use a different functionality or change how the functionality is used. It might also simply state that the functionality isn't yet compatible with trimming without a clear replacement. +### Step-by-step: Annotate reflection usage -If the guidance to the developer becomes too long to be included in a warning message, you can add an optional `Url` to the to point the developer to a web page describing the problem and possible solutions in greater detail. - -For example: +Consider this example that produces a warning: ```csharp -[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead", Url = "https://site/trimming-and-method")] -void MethodWithAssemblyLoad() { ... } -``` - -This produces a warning: - -```console -IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead. https://site/trimming-and-method -``` - -Using `RequiresUnreferencedCode` often leads to marking more methods with it, due to the same reason. This is common when a high-level method becomes incompatible with trimming because it calls a low-level method that isn't trim-compatible. You "bubble up" the warning to a public API. Each usage of `RequiresUnreferencedCode` needs a message, and in these cases the messages are likely the same. To avoid duplicating strings and making it easier to maintain, use a constant string field to store the message: - -```csharp -class Functionality +void PrintMethodNames(Type type) { - const string IncompatibleWithTrimmingMessage = "This functionality is not compatible with trimming. Use 'FunctionalityFriendlyToTrimming' instead"; - - [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)] - private void ImplementationOfAssemblyLoading() + // ⚠️ IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' + foreach (var method in type.GetMethods()) { - ... - } - - [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)] - public void MethodWithAssemblyLoad() - { - ImplementationOfAssemblyLoading(); + Console.WriteLine(method.Name); } } ``` -### Functionality with requirements on its input +**Step 1: Identify what reflection operation is performed** -Trimming provides APIs to specify more requirements on input to methods and other members that lead to trim-compatible code. These requirements are usually about reflection and the ability to access certain members or operations on a type. Such requirements are specified using the . +The code calls `GetMethods()`, which requires `PublicMethods` to be preserved. -Unlike `RequiresUnreferencedCode`, reflection can sometimes be understood by the trimmer as long as it's annotated correctly. Let's take another look at the original example: +**Step 2: Annotate the parameter** + +Add `DynamicallyAccessedMembers` to tell the trimmer what's needed: ```csharp -string s = Console.ReadLine(); -Type type = Type.GetType(s); -foreach (var m in type.GetMethods()) +void PrintMethodNames( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) { - Console.WriteLine(m.Name); + // ✅ No warning - trimmer knows to preserve public methods + foreach (var method in type.GetMethods()) + { + Console.WriteLine(method.Name); + } } ``` -In the previous example, the real problem is `Console.ReadLine()`. Because *any* type could be read, the trimmer has no way to know if you need methods on `System.DateTime` or `System.Guid` or any other type. On the other hand, the following code would be fine: +**Step 3: Ensure callers satisfy the requirement** + +When calling this method with a known type (`typeof`), the requirement is automatically satisfied: ```csharp -Type type = typeof(System.DateTime); -foreach (var m in type.GetMethods()) -{ - Console.WriteLine(m.Name); -} +// ✅ OK - DateTime's public methods will be preserved +PrintMethodNames(typeof(DateTime)); ``` -Here the trimmer can see the exact type being referenced: `System.DateTime`. Now it can use flow analysis to determine that it needs to keep all public methods on `System.DateTime`. So where does `DynamicallyAccessMembers` come in? When reflection is split across multiple methods. In the following code, we can see that the type `System.DateTime` flows to `Method3` where reflection is used to access `System.DateTime`'s methods, +### Step-by-step: Propagate requirements through call chains + +When types flow through multiple methods, you need to propagate requirements: ```csharp void Method1() { - Method2(); + Method2(); // ⚠️ Warning: Generic parameter needs annotation } + void Method2() { Type t = typeof(T); - Method3(t); + Method3(t); // ⚠️ Warning: Argument doesn't satisfy requirements } + void Method3(Type type) { - var methods = type.GetMethods(); - ... + var methods = type.GetMethods(); // ⚠️ Warning: Reflection usage } ``` -If you compile the previous code, the following warning is produced: +**Step 1: Start at the reflection usage** -> IL2070: Program.Method3(Type): 'this' argument does not satisfy -> 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethods()'. The -> parameter 'type' of method 'Program.Method3(Type)' does not have matching annotations. The -> source value must declare at least the same requirements as those declared on the target -> location it is assigned to. - -For performance and stability, flow analysis isn't performed between methods, so an annotation is needed to pass information between methods, from the reflection call (`GetMethods`) to the source of the `Type`. In the previous example, the trimmer warning is saying that `GetMethods` requires the `Type` object instance it's called on to have the `PublicMethods` annotation, but the `type` variable doesn't have the same requirement. In other words, we need to pass the requirements from `GetMethods` up to the caller: +Annotate where reflection is actually used: ```csharp -void Method1() +void Method3( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) { - Method2(); + var methods = type.GetMethods(); // ✅ Fixed } -void Method2() +``` + +**Step 2: Propagate up the call chain** + +Work backward through the call chain: + +```csharp +void Method2<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>() { Type t = typeof(T); - Method3(t); + Method3(t); // ✅ Fixed - T is annotated } -void Method3( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) +``` + +**Step 3: Verify at the call site** + +```csharp +void Method1() { - var methods = type.GetMethods(); - ... + Method2(); // ✅ Fixed - DateTime's public methods preserved } ``` -After annotating the parameter `type`, the original warning disappears, but another appears: +For more details on how requirements flow through code, see [Understanding trim analysis](trimming-concepts.md). + +### Common DynamicallyAccessedMemberTypes values -> IL2087: 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' -> in call to 'Program.Method3(Type)'. The generic parameter 'T' of 'Program.Method2\()' does not -> have matching annotations. +Choose the minimum access level needed: -We propagated annotations up to the parameter `type` of `Method3`, in `Method2` we have a similar issue. The trimmer is able to track the value `T` as it flows through the call to `typeof`, is assigned to the local variable `t`, and passed to `Method3`. At that point it sees that the parameter `type` requires `PublicMethods` but there are no requirements on `T`, and produces a new warning. To fix this, we must "annotate and propagate" by applying annotations all the way up the call chain until we reach a statically known type (like `System.DateTime` or `System.Tuple`), or another annotated value. In this case, we need to annotate the type parameter `T` of `Method2`. +| Member Type | When to use | +| ----------- | ----------- | +| `PublicConstructors` | Using `Activator.CreateInstance()` or `GetConstructor()` | +| `PublicMethods` | Using `GetMethod()` or `GetMethods()` | +| `PublicFields` | Using `GetField()` or `GetFields()` | +| `PublicProperties` | Using `GetProperty()` or `GetProperties()` (serialization) | +| `PublicEvents` | Using `GetEvent()` or `GetEvents()` | + +> [!WARNING] +> Using `DynamicallyAccessedMemberTypes.All` preserves all members on the target type and all members on its nested types (but not transitive dependencies like members on a property's return type). This significantly increases app size. More importantly, preserved members become reachable, which means they may contain their own problematic code. For example, if a preserved member calls a method marked with `RequiresUnreferencedCode`, that warning cannot be resolved since the member is being kept through reflection annotation rather than an explicit call. Use the minimum required member types to avoid these cascading issues. + +## Approach 3: Mark code as incompatible with RequiresUnreferencedCode + +When code fundamentally cannot be made analyzable, use to document the incompatibility. + +### When to use RequiresUnreferencedCode + +Use this attribute when: + +- **Types are loaded dynamically**: Using with runtime-determined strings. +- **Assemblies are loaded at runtime**: Using . +- **Complex reflection patterns**: Reflection usage too complex to annotate. +- **Runtime code generation**: Using or the `dynamic` keyword. + +### Step-by-step: Mark incompatible methods + +**Step 1: Identify truly incompatible code** + +Example of code that cannot be made trim-compatible: ```csharp -void Method1() +void LoadPluginByName(string pluginName) { - Method2(); + // Type name comes from runtime input - trimmer cannot know what types are needed + Type pluginType = Type.GetType(pluginName); + var plugin = Activator.CreateInstance(pluginType); + // Use plugin... } -void Method2<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>() +``` + +**Step 2: Add RequiresUnreferencedCode attribute** + +```csharp +[RequiresUnreferencedCode("Plugin loading by name is not compatible with trimming. Consider using compile-time plugin registration instead.")] +void LoadPluginByName(string pluginName) { - Type t = typeof(T); - Method3(t); + Type pluginType = Type.GetType(pluginName); + var plugin = Activator.CreateInstance(pluginType); + // ✅ No warnings inside this method - it's marked as incompatible } -void Method3( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) +``` + +**Step 3: Callers receive warnings** + +```csharp +void InitializePlugins() { - var methods = type.GetMethods(); - ... + // ⚠️ IL2026: Using member 'LoadPluginByName' which has 'RequiresUnreferencedCodeAttribute' + // can break functionality when trimming application code. Plugin loading by name is not + // compatible with trimming. Consider using compile-time plugin registration instead. + LoadPluginByName("MyPlugin"); } ``` -Now there are no warnings because the trimmer knows which members might be accessed via runtime reflection (public methods) and on which types (`System.DateTime`), and it preserves them. It's best practice to add annotations so the trimmer knows what to preserve. +### Writing effective warning messages -Warnings produced by these extra requirements are automatically suppressed if the affected code is in a method with `RequiresUnreferencedCode`. +A good `RequiresUnreferencedCode` message should: -Unlike `RequiresUnreferencedCode`, which simply reports the incompatibility, adding `DynamicallyAccessedMembers` makes the code compatible with trimming. +- **State what functionality is incompatible**: Be specific about what doesn't work with trimming. +- **Suggest alternatives**: Guide developers toward trim-compatible solutions. +- **Be concise**: Keep messages short and actionable. -> [!NOTE] -> Using `DynamicallyAccessedMembersAttribute` will root all the specified `DynamicallyAccessedMemberTypes` members of the type. This means it will keep the members, as well as any metadata referenced by those members. This can lead to much larger apps than expected. Be careful to use the minimum `DynamicallyAccessedMemberTypes` required. +```csharp +// ❌ Not helpful +[RequiresUnreferencedCode("Uses reflection")] -### Suppressing trimmer warnings +// ✅ Helpful - explains problem and suggests alternative +[RequiresUnreferencedCode("Dynamic type loading is not compatible with trimming. Use generic type parameters or source generators instead.")] +``` -If you can somehow determine that the call is safe, and all the code that's needed won't be trimmed away, you can also suppress the warning using . For example: +For longer guidance, add a `Url` parameter: ```csharp -[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")] -void MethodWithAssemblyLoad() { ... } +[RequiresUnreferencedCode( + "Plugin system is not compatible with trimming. See documentation for alternatives.", + Url = "https://docs.example.com/plugin-trimming")] +``` + +### Propagating RequiresUnreferencedCode -[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", - Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] -void TestMethod() +When a method calls another method marked with `RequiresUnreferencedCode`, you typically need to propagate the attribute: + +```csharp +class PluginSystem +{ + // Use a constant for consistent messaging + const string PluginMessage = "Plugin system is not compatible with trimming. Use compile-time registration instead."; + + [RequiresUnreferencedCode(PluginMessage)] + private void LoadPluginImplementation(string name) + { + // Low-level plugin loading + } + + [RequiresUnreferencedCode(PluginMessage)] + public void LoadPlugin(string name) + { + LoadPluginImplementation(name); // ✅ No warning - method is also marked + } +} +``` + +## Common patterns and solutions + +### Pattern: Factory methods with Activator.CreateInstance + +```csharp +// ❌ Before: Produces warning +object CreateInstance(Type type) +{ + return Activator.CreateInstance(type); +} + +// ✅ After: Trim-compatible +object CreateInstance( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type) +{ + return Activator.CreateInstance(type); +} +``` + +### Pattern: Plugin systems loading assemblies + +```csharp +// This pattern is fundamentally incompatible with trimming +[RequiresUnreferencedCode("Plugin loading is not compatible with trimming. Consider compile-time plugin registration using source generators.")] +void LoadPluginsFromDirectory(string directory) { - InitializeEverything(); + foreach (var dll in Directory.GetFiles(directory, "*.dll")) + { + Assembly.LoadFrom(dll); + } +} +``` - MethodWithAssemblyLoad(); // Warning suppressed +### Pattern: Dependency injection containers - ReportResults(); +```csharp +// Complex DI containers are often incompatible +class Container +{ + [RequiresUnreferencedCode("Service resolution uses complex reflection. Consider using source-generated DI or registering services explicitly.")] + public object Resolve(Type serviceType) + { + // Complex reflection to resolve dependencies + } } ``` +## Approach 4: Suppress warnings as last resort + > [!WARNING] -> Be very careful when suppressing trim warnings. It's possible that the call may be trim-compatible now, but as you change your code that may change, and you may forget to review all the suppressions. +> Only suppress trim warnings if you're absolutely certain the code is safe. Incorrect suppressions can lead to runtime failures after trimming. -`UnconditionalSuppressMessage` is like `SuppressMessage` but it can be seen by `publish` and other post-build tools. +Use when you've verified that code is trim-safe but the trimmer cannot prove it statically. + +### When suppression is appropriate + +Suppress warnings only when: + +1. You've manually ensured all required code is preserved (via `DynamicDependency` or other mechanisms). +2. The code path is never executed in trimmed scenarios. +3. You've thoroughly tested the trimmed application. + +### How to suppress warnings + +```csharp +[RequiresUnreferencedCode("Uses reflection")] +void MethodWithReflection() { /* ... */ } + +[UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "All referenced types are manually preserved via DynamicDependency attributes")] +void CallerMethod() +{ + MethodWithReflection(); // Warning suppressed +} +``` > [!IMPORTANT] -> Do not use `SuppressMessage` or `#pragma warning disable` to suppress trimmer warnings. These only work for the compiler, but are not preserved in the compiled assembly. Trimmer operates on compiled assemblies and would not see the suppression. +> Do not use `SuppressMessage` or `#pragma warning disable` for trim warnings. These only work for the compiler but aren't preserved in the compiled assembly. The trimmer operates on compiled assemblies and won't see these suppressions. Always use `UnconditionalSuppressMessage`. -The suppression applies to the entire method body. So in our sample above it suppresses all `IL2026` warnings from the method. This makes it harder to understand, as it's not clear which method is the problematic one, unless you add a comment. More importantly, if the code changes in the future, such as if `ReportResults` becomes trim-incompatible as well, no warning is reported for this method call. +### Minimize suppression scope -You can resolve this by refactoring the problematic method call into a separate method or local function and then applying the suppression to just that method: +Apply suppressions to the smallest scope possible. Extract the problematic call into a local function: ```csharp -void TestMethod() +void ProcessData() { - InitializeEverything(); + InitializeData(); - CallMethodWithAssemblyLoad(); + CallReflectionMethod(); // Only this call is suppressed - ReportResults(); + ProcessResults(); - [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", - Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] - void CallMethodWithAssemblyLoad() + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "Types are preserved via DynamicDependency on ProcessData method")] + void CallReflectionMethod() { - MethodWIthAssemblyLoad(); // Warning suppressed + MethodWithReflection(); } } ``` + +This approach: + +- Makes it clear which specific call is suppressed. +- Prevents accidentally suppressing other warnings if the code changes. +- Keeps the justification close to the suppressed call. + +## Troubleshooting tips + +### Warning persists after adding DynamicallyAccessedMembers + +Ensure you've annotated the entire call chain from the reflection usage back to the source of the `Type`: + +1. Find where reflection is used (like `GetMethods()`). +2. Annotate that method's parameter. +3. Follow the `Type` value backward through all method calls. +4. Annotate each parameter, field, or generic type parameter in the chain. + +### Too many warnings to address + +1. Start with your own code - fix warnings in code you control first. +2. Use `TrimmerSingleWarn` to see individual warnings from packages. +3. Consider if trimming is appropriate for your application. +4. Check [Known trimming incompatibilities](incompatibilities.md) for framework-level issues. + +### Unsure which DynamicallyAccessedMemberTypes to use + +Look at the reflection API being called: + +- `GetMethod()` / `GetMethods()` → `PublicMethods` +- `GetProperty()` / `GetProperties()` → `PublicProperties` +- `GetField()` / `GetFields()` → `PublicFields` +- `GetConstructor()` / `Activator.CreateInstance()` → `PublicParameterlessConstructor` or `PublicConstructors` +- `GetEvent()` / `GetEvents()` → `PublicEvents` + +Use the narrowest type possible to minimize app size. + +## Next steps + +- [Understanding trim analysis](trimming-concepts.md) - Learn the fundamental concepts behind trim warnings +- [Prepare libraries for trimming](prepare-libraries-for-trimming.md) - Make your libraries trim-compatible +- [Trim warning reference](trim-warnings/il2026.md) - Detailed information about specific warning codes +- [Known incompatibilities](incompatibilities.md) - Patterns that cannot be made trim-compatible diff --git a/docs/core/deploying/trimming/trim-self-contained.md b/docs/core/deploying/trimming/trim-self-contained.md index d33ca4a1f80ad..29588dce3e9bc 100644 --- a/docs/core/deploying/trimming/trim-self-contained.md +++ b/docs/core/deploying/trimming/trim-self-contained.md @@ -11,7 +11,7 @@ The [framework-dependent deployment model](../index.md#framework-dependent-deplo The trim-self-contained deployment model is a specialized version of the self-contained deployment model that is optimized to reduce deployment size. Minimizing deployment size is a critical requirement for some client-side scenarios like Blazor applications. Depending on the complexity of the application, only a subset of the framework assemblies are referenced, and a subset of the code within each assembly is required to run the application. The unused parts of the libraries are unnecessary and can be trimmed from the packaged application. -However, there is a risk that the build-time analysis of the application can cause failures at run time, due to not being able to reliably analyze various problematic code patterns (largely centered on reflection use). To mitigate these problems, warnings are produced whenever the trimmer cannot fully analyze a code pattern. For information on what the trim warnings mean and how to resolve them, see [Introduction to trim warnings](fixing-warnings.md). +However, there is a risk that the build-time analysis of the application can cause failures at run time, due to not being able to reliably analyze various problematic code patterns (largely centered on reflection use). To mitigate these problems, warnings are produced whenever the trimmer cannot fully analyze a code pattern. For information on what the trim warnings mean and how to resolve them, see [Fix trim warnings](fixing-warnings.md). To understand how trimming works and why certain patterns cause warnings, see [Understanding trim analysis](trimming-concepts.md). > [!NOTE] > @@ -23,7 +23,7 @@ However, there is a risk that the build-time analysis of the application can cau > [!WARNING] > Not all project types can be trimmed. For more information, see [Known trimming incompatibilities](incompatibilities.md). -Any code that causes build time analysis challenges isn't suitable for trimming. Some common coding patterns that are problematic when used by an application originate from unbounded reflection usage and external dependencies that aren't visible at build time. An example of unbounded reflection is a legacy serializer, such as [XML serialization](../../../standard/serialization/introducing-xml-serialization.md), and an example of invisible external dependencies is [built-in COM](../../../standard/native-interop/cominterop.md). To address trim warnings in your application, see [Introduction to trim warnings](fixing-warnings.md), and to make your library compatible with trimming, see [Prepare .NET libraries for trimming](prepare-libraries-for-trimming.md). +Any code that causes build time analysis challenges isn't suitable for trimming. Some common coding patterns that are problematic when used by an application originate from unbounded reflection usage and external dependencies that aren't visible at build time. An example of unbounded reflection is a legacy serializer, such as [XML serialization](../../../standard/serialization/introducing-xml-serialization.md), and an example of invisible external dependencies is [built-in COM](../../../standard/native-interop/cominterop.md). To address trim warnings in your application, see [Fix trim warnings](fixing-warnings.md), and to make your library compatible with trimming, see [Prepare .NET libraries for trimming](prepare-libraries-for-trimming.md). ## Enable trimming @@ -69,7 +69,17 @@ For more information, see [.NET application publishing overview](../../deploying For more information, see [.NET application publishing overview](../../deploying/index.md). +## Next steps + +After enabling trimming, you might encounter trim warnings during build. Follow these guides to understand and resolve them: + +- **[Understanding trim analysis](trimming-concepts.md)** - Learn how the trimmer works and why certain code patterns produce warnings. This conceptual guide explains the fundamental principles of trim analysis. +- **[Fix trim warnings](fixing-warnings.md)** - Step-by-step workflows for resolving trim warnings in your code. +- **[Prepare libraries for trimming](prepare-libraries-for-trimming.md)** - Make your libraries compatible with trimming. +- **[Trimming options](trimming-options.md)** - Reference for MSBuild properties that control trimming behavior. + ## See also -- [.NET application publishing overview](../../deploying/index.md). -- [dotnet publish command](../../tools/dotnet-publish.md). +- [.NET application publishing overview](../../deploying/index.md) +- [dotnet publish command](../../tools/dotnet-publish.md) +- [Known trimming incompatibilities](incompatibilities.md) diff --git a/docs/core/deploying/trimming/trimming-concepts.md b/docs/core/deploying/trimming/trimming-concepts.md new file mode 100644 index 0000000000000..03992998e0037 --- /dev/null +++ b/docs/core/deploying/trimming/trimming-concepts.md @@ -0,0 +1,402 @@ +--- +title: Understanding trim analysis +description: Learn the fundamental concepts of how trim analysis works, why certain patterns produce warnings, and how to make code trim-compatible from first principles. +author: agocke +ms.author: angocke +ms.date: 11/04/2025 +ai-usage: ai-assisted +--- +# Understanding trim analysis + +This article explains the fundamental concepts behind trim analysis to help you understand why certain code patterns produce warnings and how to make your code trim-compatible. Understanding these concepts will help you make informed decisions when addressing trim warnings rather than simply "spreading attributes around to silence the tooling." + +## How the trimmer analyzes code + +The trimmer performs **static analysis** at publish time to determine which code is used by your application. It starts from known entry points (like your `Main` method) and follows the code paths through your application. + +### What the trimmer can understand + +The trimmer excels at analyzing direct, compile-time-visible code patterns: + +```csharp +// The trimmer CAN understand these patterns: +var date = new DateTime(); +date.AddDays(1); // Direct method call - trimmer knows AddDays is used + +var list = new List(); +list.Add("hello"); // Generic method call - trimmer knows List.Add is used + +string result = MyUtility.Process("data"); // Direct static method call +``` + +In these examples, the trimmer can follow the code path and mark `DateTime.AddDays`, `List.Add`, and `MyUtility.Process` as used code that should be kept in the final application. + +### What the trimmer cannot understand + +The trimmer struggles with dynamic operations where the target of an operation isn't known until runtime: + +```csharp +// The trimmer CANNOT fully understand these patterns: +Type type = Type.GetType(Console.ReadLine()); // Type name from user input +type.GetMethod("SomeMethod"); // Which method? On which type? + +object obj = GetSomeObject(); +obj.GetType().GetProperties(); // What type will obj be at runtime? + +Assembly asm = Assembly.LoadFrom(pluginPath); // What's in this assembly? +``` + +In these examples, the trimmer has no way to know: + +- Which type the user will enter +- What type `GetSomeObject()` returns +- What code exists in the dynamically loaded assembly + +This is the fundamental problem that trim warnings address. + +## The reflection problem + +Reflection allows code to inspect and invoke types and members dynamically at runtime. This is powerful but creates a challenge for static analysis. + +### Why reflection breaks trimming + +Consider this example: + +```csharp +void PrintMethodNames(Type type) +{ + foreach (var method in type.GetMethods()) + { + Console.WriteLine(method.Name); + } +} + +// Called somewhere in the app +PrintMethodNames(typeof(DateTime)); +``` + +From the trimmer's perspective: + +- It sees `type.GetMethods()` is called. +- It doesn't know what `type` will be (it's a parameter). +- It can't determine which types' methods need to be preserved. +- Without guidance, it might remove methods from `DateTime`, breaking the code. + +Consequently, the trimmer produces a warning on this code. + +## Understanding DynamicallyAccessedMembers + + solves the reflection problem by creating an explicit contract between the caller and the called method. + +### The fundamental purpose + +`DynamicallyAccessedMembers` tells the trimmer: "This parameter (or field, or return value) will hold a `Type` that needs specific members preserved because reflection will be used to access them." + +### A concrete example + +Let's fix the previous example: + +```csharp +void PrintMethodNames( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) +{ + foreach (var method in type.GetMethods()) + { + Console.WriteLine(method.Name); + } +} + +// When this is called... +PrintMethodNames(typeof(DateTime)); +``` + +Now the trimmer understands: + +1. `PrintMethodNames` requires its parameter to have `PublicMethods` preserved. +2. The call site passes `typeof(DateTime)`. +3. Therefore, `DateTime`'s public methods must be kept. + +The attribute creates a **requirement** that flows backward from the reflection usage to the source of the `Type` value. + +### It's a contract, not a hint + +This is crucial to understand: `DynamicallyAccessedMembers` isn't just documentation. The trimmer enforces this contract. + +#### Analogy with generic type constraints + +If you're familiar with generic type constraints, `DynamicallyAccessedMembers` works similarly. Just as generic constraints flow through your code: + +```csharp +void Process(T value) where T : IDisposable +{ + value.Dispose(); // OK because constraint guarantees IDisposable +} + +void CallProcess(T value) where T : IDisposable +{ + Process(value); // OK - constraint satisfied +} + +void CallProcessBroken(T value) +{ + Process(value); // ERROR - T doesn't have IDisposable constraint +} +``` + +`DynamicallyAccessedMembers` creates similar requirements that flow through your code: + +```csharp +void UseReflection([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) +{ + type.GetMethods(); // OK because annotation guarantees methods are preserved +} + +void PassType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) +{ + UseReflection(type); // OK - requirement satisfied +} + +void PassTypeBroken(Type type) +{ + UseReflection(type); // WARNING - type doesn't have required annotation +} +``` + +Both create contracts that must be fulfilled, and both produce errors or warnings when the contract can't be satisfied. + +#### How the contract is enforced + +```csharp +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] +Type GetTypeForProcessing() +{ + return typeof(DateTime); // OK - trimmer will preserve DateTime's public methods +} + +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] +Type GetTypeFromInput() +{ + // WARNING: The trimmer can't verify that the type from GetType() + // will have its public methods preserved + return Type.GetType(Console.ReadLine()); +} +``` + +If you can't fulfill the contract (like in the second example), you'll get a warning. + +## Understanding RequiresUnreferencedCode + +Some code patterns simply cannot be made statically analyzable. For these cases, use . + +### When to use RequiresUnreferencedCode + +Use the attribute when: + +- **The reflection pattern is fundamentally dynamic**: Loading assemblies or types by string names from external sources. +- **The complexity is too high to annotate**: Code that uses reflection in complex, data-driven ways. +- **You're using runtime code generation**: Technologies like or the `dynamic` keyword. + +Example: + +```csharp +[RequiresUnreferencedCode("Plugin loading is not compatible with trimming")] +void LoadPlugin(string pluginPath) +{ + Assembly pluginAssembly = Assembly.LoadFrom(pluginPath); + // Plugin assemblies aren't known at publish time + // This fundamentally cannot be made trim-compatible +} +``` + +### The purpose of the attribute + +`RequiresUnreferencedCode` serves two purposes: + +1. **Suppresses warnings inside the method**: The trimmer won't analyze or warn about the reflection usage. +2. **Creates warnings at call sites**: Any code calling this method gets a warning. + +This "bubbles up" the warning to give developers visibility into trim-incompatible code paths. + +### Writing good messages + +The message should help developers understand their options: + +```csharp +// ❌ Not helpful +[RequiresUnreferencedCode("Uses reflection")] + +// ✅ Helpful - explains what's incompatible and suggests alternatives +[RequiresUnreferencedCode("Plugin loading is not compatible with trimming. Consider using a source generator for known plugins instead")] +``` + +## How requirements flow through code + +Understanding how requirements propagate helps you know where to add attributes. + +### Requirements flow backward + +Requirements flow from where reflection is used back to where the `Type` originates: + +```csharp +void CallChain() +{ + // Step 1: Source of the Type value + ProcessData(); // ← Requirement ends here +} + +void ProcessData() +{ + // Step 2: Type flows through generic parameter + var type = typeof(T); + DisplayInfo(type); // ← Requirement flows back through here +} + +void DisplayInfo(Type type) +{ + // Step 3: Reflection creates the requirement + type.GetMethods(); // ← Requirement starts here +} +``` + +To make this trim-compatible, you need to annotate the chain: + +```csharp +void ProcessData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>() +{ + var type = typeof(T); + DisplayInfo(type); +} + +void DisplayInfo( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) +{ + type.GetMethods(); +} +``` + +Now the requirement flows: `GetMethods()` requires `PublicMethods` → `type` parameter needs `PublicMethods` → generic `T` needs `PublicMethods` → `DateTime` needs `PublicMethods` preserved. + +### Requirements flow through storage + +Requirements also flow through fields and properties: + +```csharp +class TypeHolder +{ + // This field will hold Types that need PublicMethods preserved + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] + private Type _typeToProcess; + + public void SetType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>() + { + _typeToProcess = typeof(T); // OK - requirement satisfied + } + + public void Process() + { + _typeToProcess.GetMethods(); // OK - field is annotated + } +} +``` + +## Choosing the right approach + +When you encounter code that needs reflection, follow this decision tree: + +### 1. Can you avoid reflection? + +The best solution is to avoid reflection when possible: + +```csharp +// ❌ Uses reflection +void Process(Type type) +{ + var instance = Activator.CreateInstance(type); +} + +// ✅ Uses compile-time generics instead +void Process() where T : new() +{ + var instance = new T(); +} +``` + +### 2. Is the Type known at compile time? + +If reflection is necessary but the types are known, use `DynamicallyAccessedMembers`: + +```csharp +// ✅ Trim-compatible +void Serialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T obj) +{ + foreach (var prop in typeof(T).GetProperties()) + { + // Serialize property + } +} +``` + +### 3. Is the pattern fundamentally dynamic? + +If the types truly aren't known until runtime, use `RequiresUnreferencedCode`: + +```csharp +// ✅ Documented as trim-incompatible +[RequiresUnreferencedCode("Dynamic type loading is not compatible with trimming")] +void ProcessTypeByName(string typeName) +{ + var type = Type.GetType(typeName); + // Work with type +} +``` + +## Common patterns and solutions + +### Pattern: Factory methods + +```csharp +// Problem: Creating instances from Type parameter +object CreateInstance(Type type) +{ + return Activator.CreateInstance(type); +} + +// Solution: Specify constructor requirements +object CreateInstance( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type) +{ + return Activator.CreateInstance(type); +} +``` + +### Pattern: Plugin systems + +```csharp +// Problem: Loading unknown assemblies at runtime +[RequiresUnreferencedCode("Plugin loading is not trim-compatible. Plugins must be known at compile time.")] +void LoadPlugins(string pluginDirectory) +{ + foreach (var file in Directory.GetFiles(pluginDirectory, "*.dll")) + { + Assembly.LoadFrom(file); + } +} + +// Better solution: Known plugins with source generation +// Use source generators to create plugin registration code at compile time +``` + +## Key takeaways + +- **The trimmer uses static analysis** - it can only understand code paths visible at compile time. +- **Reflection breaks static analysis** - the trimmer can't see what reflection will access at runtime. +- **DynamicallyAccessedMembers creates contracts** - it tells the trimmer what needs to be preserved. +- **Requirements flow backward** - from reflection usage back to the source of the `Type` value. +- **RequiresUnreferencedCode documents incompatibility** - use it when code can't be made analyzable. +- **Attributes aren't just hints** - the trimmer enforces contracts and produces warnings when they can't be met. + +## Next steps + +- [Fix trim warnings](fixing-warnings.md) - Apply these concepts to resolve warnings in your code +- [Prepare libraries for trimming](prepare-libraries-for-trimming.md) - Make your libraries trim-compatible +- [Trim warning reference](trim-warnings/il2026.md) - Detailed information about specific warnings diff --git a/docs/navigate/devops-testing/toc.yml b/docs/navigate/devops-testing/toc.yml index 0cc2f22342903..522deae933da2 100644 --- a/docs/navigate/devops-testing/toc.yml +++ b/docs/navigate/devops-testing/toc.yml @@ -319,19 +319,27 @@ items: href: ../../core/deploying/macos.md - name: Trim self-contained deployments items: - - name: Overview and how-to + - name: Overview href: ../../core/deploying/trimming/trim-self-contained.md - - name: Intro to trim warnings - href: ../../core/deploying/trimming/fixing-warnings.md - - name: Trim incompatibilities - href: ../../core/deploying/trimming/incompatibilities.md - - name: Options - href: ../../core/deploying/trimming/trimming-options.md - - name: Trimming libraries - href: ../../core/deploying/trimming/prepare-libraries-for-trimming.md - - name: Intrinsic APIs marked RequiresUnreferencedCode - href: ../../core/deploying/trimming/intrinsic-requiresunreferencedcode-apis.md - - name: Trim warnings + - name: Concepts + items: + - name: Understand trim analysis + href: ../../core/deploying/trimming/trimming-concepts.md + - name: How-to guides + items: + - name: Fix trim warnings + href: ../../core/deploying/trimming/fixing-warnings.md + - name: Prepare libraries for trimming + href: ../../core/deploying/trimming/prepare-libraries-for-trimming.md + - name: Reference + items: + - name: Trimming options + href: ../../core/deploying/trimming/trimming-options.md + - name: Known incompatibilities + href: ../../core/deploying/trimming/incompatibilities.md + - name: Intrinsic RequiresUnreferencedCode APIs + href: ../../core/deploying/trimming/intrinsic-requiresunreferencedcode-apis.md + - name: Trim warning reference items: - name: IL2001 href: ../../core/deploying/trimming/trim-warnings/il2001.md From 8245711b6cc337d3d1688104de5c3f3377a1314c Mon Sep 17 00:00:00 2001 From: Megha Anand <88059806+anandmeg@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:51:41 -0800 Subject: [PATCH 4/4] Revise AI example (#49914) --- .../serialization/system-text-json/deserialization.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/standard/serialization/system-text-json/deserialization.md b/docs/standard/serialization/system-text-json/deserialization.md index 03cee6a725127..cb1cf82a10a0b 100644 --- a/docs/standard/serialization/system-text-json/deserialization.md +++ b/docs/standard/serialization/system-text-json/deserialization.md @@ -89,13 +89,13 @@ To deserialize from UTF-8, call a