diff --git a/docs/core/compatibility/10.0.md b/docs/core/compatibility/10.0.md index 1eb3ffccd047e..c5908109757a9 100644 --- a/docs/core/compatibility/10.0.md +++ b/docs/core/compatibility/10.0.md @@ -107,6 +107,7 @@ If you're migrating an app to .NET 10, the breaking changes listed here might af | [Default workload configuration from 'loose manifests' to 'workload sets' mode](sdk/10.0/default-workload-config.md) | Behavioral change | Preview 2 | | [`dotnet package list` performs restore](sdk/10.0/dotnet-package-list-restore.md) | Behavioral change | Preview 4 | | [`dotnet restore` audits transitive packages](sdk/10.0/nugetaudit-transitive-packages.md) | Behavioral change | Preview 3 | +| [project.json not supported in `dotnet restore`](sdk/10.0/dotnet-restore-project-json-unsupported.md) | Source incompatible | Preview 7 | | [SHA-1 fingerprint support deprecated in `dotnet nuget sign`](sdk/10.0/dotnet-nuget-sign-sha1-deprecated.md) | Behavioral change | Preview 1 | | [MSBUILDCUSTOMBUILDEVENTWARNING escape hatch removed](sdk/10.0/custom-build-event-warning.md) | Behavioral change | Preview 1 | | [MSBuild custom culture resource handling](sdk/10.0/msbuild-custom-culture.md) | Behavioral change | Preview 1 | diff --git a/docs/core/compatibility/sdk/10.0/dotnet-restore-project-json-unsupported.md b/docs/core/compatibility/sdk/10.0/dotnet-restore-project-json-unsupported.md new file mode 100644 index 0000000000000..4feb15a2d811b --- /dev/null +++ b/docs/core/compatibility/sdk/10.0/dotnet-restore-project-json-unsupported.md @@ -0,0 +1,47 @@ +--- +title: "Breaking change - project.json is no longer supported in dotnet restore" +description: "Learn about the breaking change in .NET 10 where dotnet restore no longer supports project.json based projects." +ms.date: 08/16/2025 +ai-usage: ai-assisted +ms.custom: https://github.com/dotnet/docs/issues/47968 +--- + +# project.json no longer supported in dotnet restore + +Starting with .NET 10, the [`dotnet restore` command](../../../tools/dotnet-restore.md) no longer supports `project.json` based projects. Such projects are ignored during the restore operation. + +## Version introduced + +.NET 10 Preview 7 + +## Previous behavior + +The `dotnet restore` command restored dependencies for `project.json` based projects. + +## New behavior + +The `dotnet restore` command ignores `project.json` based projects and no longer restores their dependencies. + +## Type of breaking change + +This change can affect [source compatibility](../../categories.md#source-compatibility). + +## Reason for change + +The `project.json` format was originally available only in .NET Core previews (through Preview 2 of .NET Core 1.0) and was completely replaced by PackageReference in 2017. The format has been marked as deprecated since 2017. + +When the `project.json` format was replaced, users migrated these projects using the [`dotnet migrate`](../../../tools/dotnet-migrate.md) command, but that command was removed from the CLI in the .NET Core 3.0 SDK. + +The removal of `project.json` support completes this transition and allows the .NET team to focus on delivering a better experience for PackageReference-based projects. + +## Recommended action + +Migrate your `project.json` projects to use PackageReference format instead. + +If you have .NET Core based `project.json` projects, you can use older versions of the .NET SDK that still include the [`dotnet migrate` command](../../../tools/dotnet-migrate.md) to convert them to the modern project format. + +For more information about migrating from `project.json`, see [Migrating from project.json to .csproj](/nuget/archive/project-json#migrate-projectjson-to-packagereference). + +## Affected APIs + +None. diff --git a/docs/core/compatibility/toc.yml b/docs/core/compatibility/toc.yml index 724e1f2485cca..e70cdffbc642a 100644 --- a/docs/core/compatibility/toc.yml +++ b/docs/core/compatibility/toc.yml @@ -114,6 +114,8 @@ items: href: sdk/10.0/dotnet-tool-pack-publish.md - name: "`dotnet restore` audits transitive packages" href: sdk/10.0/nugetaudit-transitive-packages.md + - name: project.json not supported in `dotnet restore` + href: sdk/10.0/dotnet-restore-project-json-unsupported.md - name: Default workload configuration from 'loose manifests' to 'workload sets' mode href: sdk/10.0/default-workload-config.md - name: "`dotnet package list` performs restore" diff --git a/docs/csharp/fundamentals/types/records.md b/docs/csharp/fundamentals/types/records.md index 80a15b191ede2..efb6c150df4fe 100644 --- a/docs/csharp/fundamentals/types/records.md +++ b/docs/csharp/fundamentals/types/records.md @@ -1,7 +1,7 @@ --- title: "Record types" description: Learn about C# record types and how to create them. A record is a class that provides value semantics. -ms.date: 05/24/2023 +ms.date: 08/15/2025 helpviewer_keywords: - "records [C#]" - "C# language, records" @@ -31,7 +31,7 @@ Immutability isn't appropriate for all data scenarios. [Entity Framework Core](/ ## How records differ from classes and structs -The same syntax that [declares](classes.md#declaring-classes) and [instantiates](classes.md#creating-objects) classes or structs can be used with records. Just substitute the `class` keyword with the `record`, or use `record struct` instead of `struct`. Likewise, the same syntax for expressing inheritance relationships is supported by record classes. Records differ from classes in the following ways: +The same syntax that [declares](classes.md#declaring-classes) and [instantiates](classes.md#creating-objects) classes or structs can be used with records. Just substitute the `class` keyword with the `record`, or use `record struct` instead of `struct`. Likewise, record classes support the same syntax for expressing inheritance relationships. Records differ from classes in the following ways: * You can use [positional parameters](../../language-reference/builtin-types/record.md#positional-syntax-for-property-and-field-definition) in a [primary constructor](../../programming-guide/classes-and-structs/instance-constructors.md#primary-constructors) to create and instantiate a type with immutable properties. * The same methods and operators that indicate reference equality or inequality in classes (such as and `==`), indicate [value equality or inequality](../../language-reference/builtin-types/record.md#value-equality) in records. @@ -57,6 +57,8 @@ The following example demonstrates use of a `with` expression to copy an immutab :::code language="csharp" source="./snippets/records/ImmutableRecord.cs" id="ImmutableRecord"::: +In the preceding examples, all properties are independent. None of the properties are computed from other property values. A `with` expression first copies the existing record instance, then modifies any properties or fields specified in the `with` expression. Computed properties in `record` types should be computed on access, not initialized when the instance is created. Otherwise, a property could return the computed value based on the original instance, not the modified copy. If you must initialize a computed property rather than compute on access, you should consider a [`class`](./classes.md) instead of a record. + For more information, see [Records (C# reference)](../../language-reference/builtin-types/record.md). ## C# Language Specification diff --git a/docs/csharp/language-reference/builtin-types/record.md b/docs/csharp/language-reference/builtin-types/record.md index 2995ba37b741f..35a98a1ab5c02 100644 --- a/docs/csharp/language-reference/builtin-types/record.md +++ b/docs/csharp/language-reference/builtin-types/record.md @@ -1,7 +1,7 @@ --- title: "Records" description: Learn about the record modifier for class and struct types in C#. Records provide standard support for value based equality on instances of record types. -ms.date: 02/05/2025 +ms.date: 08/15/2025 f1_keywords: - "record_CSharpKeyword" helpviewer_keywords: @@ -146,6 +146,27 @@ If you need different copying behavior, you can write your own copy constructor You can't override the clone method, and you can't create a member named `Clone` in any record type. The actual name of the clone method is compiler-generated. +> [!IMPORTANT] +> In the preceding examples, all properties are independent. None of the properties are computed from other property values. A `with` expression first copies the existing record instance, then modifies any properties or fields specified in the `with` expression. Computed properties in `record` types should be computed on access, not initialized when the instance is created. Otherwise, a property could return the computed value based on the original instance, not the modified copy. + +You ensure correctness on computed properties by computing the value on access, as shown in the following declaration: + +:::code language="csharp" source="snippets/shared/RecordType.cs" id="WitherComputed"::: + +The preceding record type computes the `Distance` when accessed, as shown in the following example: + +:::code language="csharp" source="snippets/shared/RecordType.cs" id="WitherComputedUsage"::: + +Contrast that with the following declaration, where the `Distance` property is computed and cached as part of the initialization of a new instance: + +:::code language="csharp" source="snippets/shared/RecordType.cs" id="WitherInit"::: + +Because `Distance` is computed as part of initialization, the value is computed and cached before the `with` expression changes the value of `Y` in the copy. The result is that the distance is incorrect: + +:::code language="csharp" source="snippets/shared/RecordType.cs" id="WitherInitUsage"::: + +The `Distance` computation isn't expensive to compute on each access. However, some computed properties might require access to more data or more extensive computation. In those cases, instead of a record, use a `class` type and compute the cached value when one of the components changes value. + ## Built-in formatting for display Record types have a compiler-generated method that displays the names and values of public properties and fields. The `ToString` method returns a string of the following format: diff --git a/docs/csharp/language-reference/builtin-types/snippets/shared/RecordType.cs b/docs/csharp/language-reference/builtin-types/snippets/shared/RecordType.cs index e01198e5a8b4b..57a9a37e542ca 100644 --- a/docs/csharp/language-reference/builtin-types/snippets/shared/RecordType.cs +++ b/docs/csharp/language-reference/builtin-types/snippets/shared/RecordType.cs @@ -23,6 +23,9 @@ public static void Examples() var p = new Point(); (double x, double y, double z) = p; + Console.WriteLine("================================================="); + ComputedWither.ExampleUsage.Example(); + } // public record Person(string FirstName, string LastName); @@ -102,7 +105,7 @@ public static class PositionalAttributes /// map to the JSON elements "firstName" and "lastName" when /// serialized or deserialized. /// - public record Person([property: JsonPropertyName("firstName")] string FirstName, + public record Person([property: JsonPropertyName("firstName")] string FirstName, [property: JsonPropertyName("lastName")] string LastName); // @@ -335,7 +338,8 @@ protected override bool PrintMembers(StringBuilder stringBuilder) if (base.PrintMembers(stringBuilder)) { stringBuilder.Append(", "); - }; + } + ; stringBuilder.Append($"Grade = {Grade}"); return true; } @@ -426,4 +430,47 @@ public static void Main() // } } + + namespace ComputedWither + { + // + public record Point(int X, int Y) + { + public double Distance => Math.Sqrt(X * X + Y * Y); + } + // + + // + public record PointInit(int X, int Y) + { + public double Distance { get; } = Math.Sqrt(X * X + Y * Y); + } + // + + public static class ExampleUsage + { + public static void Example() + { + // + Point p1 = new Point(3, 4); + Console.WriteLine($"Original point: {p1}"); + p1 = p1 with { Y = 8 }; + Console.WriteLine($"Modified point: {p1}"); + // Output: + // Original point: Point { X = 3, Y = 4, Distance = 5 } + // Modified point: Point { X = 3, Y = 8, Distance = 8.54400374531753 } + // + + // + PointInit pt1 = new PointInit(3, 4); + Console.WriteLine($"Original point: {pt1}"); + pt1 = pt1 with { Y = 8 }; + Console.WriteLine($"Incorrect Modified point: {pt1}"); + // Output: + // Original point: PointInit { X = 3, Y = 4, Distance = 5 } + // Modified point: PointInit { X = 3, Y = 8, Distance = 5 } + // + } + } + } } diff --git a/docs/csharp/language-reference/keywords/using-directive.md b/docs/csharp/language-reference/keywords/using-directive.md index 58a1f09bf9543..60c46dfd5f1fc 100644 --- a/docs/csharp/language-reference/keywords/using-directive.md +++ b/docs/csharp/language-reference/keywords/using-directive.md @@ -82,11 +82,14 @@ Analyzers issue diagnostics if you duplicate `global` using directives in differ The `using static` directive names a type whose static members and nested types you can access without specifying a type name. Its syntax is: ```csharp +// not within a namespace using static ; ``` The `` is the name of the type whose static members and nested types can be referenced without specifying a type name. If you don't provide a fully qualified type name (the full namespace name along with the type name), C# generates compiler error [CS0246](../compiler-messages/assembly-references.md): "The type or namespace name 'type/namespace' couldn't be found (are you missing a using directive or an assembly reference?)". +If the `using static` directive is applied within the context of a namespace (either file-scoped or nested in a `namespace` block, it is not necessary to fully qualify the type. + The `using static` directive applies to any type that has static members (or nested types), even if it also has instance members. However, instance members can only be invoked through the type instance. You can access static members of a type without having to qualify the access with the type name: diff --git a/docs/csharp/language-reference/keywords/when.md b/docs/csharp/language-reference/keywords/when.md index b0aafe972e071..9c2ad43a385de 100644 --- a/docs/csharp/language-reference/keywords/when.md +++ b/docs/csharp/language-reference/keywords/when.md @@ -27,6 +27,8 @@ catch (ExceptionType [e]) when (expr) where *expr* is an expression that evaluates to a Boolean value. If it returns `true`, the exception handler executes; if `false`, it does not. +Exception filters with the `when` keyword provide several advantages over traditional exception handling approaches, including better debugging support and performance benefits. For a detailed explanation of how exception filters preserve the call stack and improve debugging, see [Exception filters vs. traditional exception handling](../statements/exception-handling-statements.md#exception-filters-vs-traditional-exception-handling). + The following example uses the `when` keyword to conditionally execute handlers for an depending on the text of the exception message. [!code-csharp[when-with-catch](~/samples/snippets/csharp/language-reference/keywords/when/catch.cs)] diff --git a/docs/csharp/language-reference/operators/with-expression.md b/docs/csharp/language-reference/operators/with-expression.md index 55ecef93d9ec0..ac63b7a024548 100644 --- a/docs/csharp/language-reference/operators/with-expression.md +++ b/docs/csharp/language-reference/operators/with-expression.md @@ -1,14 +1,14 @@ --- -title: "with expression - create new objects that are modified copies of existing objects" +title: "The with expression - create new objects that are modified copies of existing objects" description: "Learn about a with expression that performs nondestructive mutation of C# records and structures. The `with` keyword provides the means to modify one or more properties in the new object." -ms.date: 11/22/2024 +ms.date: 08/15/2025 f1_keywords: - "with_CSharpKeyword" helpviewer_keywords: - "with expression [C#]" - "with operator [C#]" --- -# with expression - Nondestructive mutation creates a new object with modified properties +# The `with` expression - Nondestructive mutation creates a new object with modified properties A `with` expression produces a copy of its operand with the specified properties and fields modified. You use the [object initializer](../../programming-guide/classes-and-structs/object-and-collection-initializers.md) syntax to specify what members to modify and their new values: @@ -20,7 +20,7 @@ The result of a `with` expression has the same run-time type as the expression's :::code language="csharp" source="snippets/with-expression/InheritanceExample.cs" ::: -In the case of a reference-type member, only the reference to a member instance is copied when an operand is copied. Both the copy and original operand have access to the same reference-type instance. The following example demonstrates that behavior: +When a member is a reference type, only the reference to a member instance is copied when an operand is copied. Both the copy and original operand have access to the same reference-type instance. The following example demonstrates that behavior: :::code language="csharp" source="snippets/with-expression/ExampleWithReferenceType.cs" ::: @@ -32,6 +32,9 @@ Any record class type has the *copy constructor*. A *copy constructor* is a cons You can't customize the copy semantics for structure types. +> [!IMPORTANT] +> In the preceding examples, all properties are independent. None of the properties are computed from other property values. A `with` expression first copies the existing record instance, then modifies any properties or fields specified in the `with` expression. Computed properties in `record` types should be computed on access, not initialized when the instance is created. Otherwise, a property could return the computed value based on the original instance, not the modified copy. For more information, see the language reference article on [`record` types](../builtin-types/record.md#nondestructive-mutation). + ## C# language specification For more information, see the following sections of the [records feature proposal note](~/_csharplang/proposals/csharp-9.0/records.md): diff --git a/docs/csharp/language-reference/statements/exception-handling-statements.md b/docs/csharp/language-reference/statements/exception-handling-statements.md index ca4111d97edcd..cdcfe5f89e328 100644 --- a/docs/csharp/language-reference/statements/exception-handling-statements.md +++ b/docs/csharp/language-reference/statements/exception-handling-statements.md @@ -95,6 +95,43 @@ You can provide several `catch` clauses for the same exception type if they dist If a `catch` clause has an exception filter, it can specify the exception type that is the same as or less derived than an exception type of a `catch` clause that appears after it. For example, if an exception filter is present, a `catch (Exception e)` clause doesn't need to be the last clause. +##### Exception filters vs. traditional exception handling + +Exception filters provide significant advantages over traditional exception handling approaches. The key difference is **when** the exception handling logic is evaluated: + +- **Exception filters (`when`)**: The filter expression is evaluated *before* the stack is unwound. This means the original call stack and all local variables remain intact during filter evaluation. +- **Traditional `catch` blocks**: The catch block executes *after* the stack is unwound, potentially losing valuable debugging information. + +Here's a comparison showing the difference: + +:::code language="csharp" source="snippets/exception-handling-statements/WhenFilterExamples.cs" id="ExceptionFilterVsIfElse"::: + +## Advantages of exception filters + +- **Better debugging experience**: Since the stack isn't unwound until a filter matches, debuggers can show the original point of failure with all local variables intact. +- **Performance benefits**: If no filter matches, the exception continues propagating without the overhead of stack unwinding and restoration. +- **Cleaner code**: Multiple filters can handle different conditions of the same exception type without requiring nested if-else statements. +- **Logging and diagnostics**: You can examine and log exception details before deciding whether to handle the exception: + +:::code language="csharp" source="snippets/exception-handling-statements/WhenFilterExamples.cs" id="DebuggingAdvantageExample"::: + +### When to use exception filters + +Use exception filters when you need to: + +- Handle exceptions based on specific conditions or properties. +- Preserve the original call stack for debugging. +- Log or examine exceptions before deciding whether to handle them. +- Handle the same exception type differently based on context. + +:::code language="csharp" source="snippets/exception-handling-statements/WhenFilterExamples.cs" id="MultipleConditionsExample"::: + +### Stack trace preservation + +Exception filters preserve the original `ex.StackTrace` property. If a `catch` clause can't process the exception and re-throws, the original stack information is lost. The `when` filter doesn't unwind the stack, so if a `when` filter is `false`, the original stack trace isn't changed. + +The exception filter approach is valuable in applications where preserving debugging information is crucial for diagnosing issues. + #### Exceptions in async and iterator methods If an exception occurs in an [async function](../keywords/async.md), it propagates to the caller of the function when you [await](../operators/await.md) the result of the function, as the following example shows: diff --git a/docs/csharp/language-reference/statements/snippets/exception-handling-statements/WhenFilterExamples.cs b/docs/csharp/language-reference/statements/snippets/exception-handling-statements/WhenFilterExamples.cs new file mode 100644 index 0000000000000..9fa9f2419b166 --- /dev/null +++ b/docs/csharp/language-reference/statements/snippets/exception-handling-statements/WhenFilterExamples.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace ExceptionFilterExamples +{ + public static class WhenFilterExamples + { + // + public static void DemonstrateStackUnwindingDifference() + { + var localVariable = "Important debugging info"; + + try + { + ProcessWithExceptionFilter(localVariable); + } + catch (InvalidOperationException ex) when (ex.Message.Contains("filter")) + { + // Exception filter: Stack not unwound yet. + // localVariable is still accessible in debugger. + // Call stack shows original throwing location. + Console.WriteLine($"Caught with filter: {ex.Message}"); + Console.WriteLine($"Local variable accessible: {localVariable}"); + } + + try + { + ProcessWithTraditionalCatch(localVariable); + } + catch (InvalidOperationException ex) + { + // Traditional catch: Stack already unwound. + // Some debugging information may be lost. + if (ex.Message.Contains("traditional")) + { + Console.WriteLine($"Caught with if: {ex.Message}"); + Console.WriteLine($"Local variable accessible: {localVariable}"); + } + else + { + throw; // Re-throws and further modifies stack trace. + } + } + } + + private static void ProcessWithExceptionFilter(string context) + { + throw new InvalidOperationException($"Exception for filter demo: {context}"); + } + + private static void ProcessWithTraditionalCatch(string context) + { + throw new InvalidOperationException($"Exception for traditional demo: {context}"); + } + // + + // + public static void HandleFileOperations(string filePath) + { + try + { + // Simulate file operation that might fail. + ProcessFile(filePath); + } + catch (IOException ex) when (ex.Message.Contains("access denied")) + { + Console.WriteLine("File access denied. Check permissions."); + } + catch (IOException ex) when (ex.Message.Contains("not found")) + { + Console.WriteLine("File not found. Verify the path."); + } + catch (IOException ex) when (IsNetworkPath(filePath)) + { + Console.WriteLine($"Network file operation failed: {ex.Message}"); + } + catch (IOException) + { + Console.WriteLine("Other I/O error occurred."); + } + } + + private static void ProcessFile(string filePath) + { + // Simulate different types of file exceptions. + if (filePath.Contains("denied")) + throw new IOException("File access denied"); + if (filePath.Contains("missing")) + throw new IOException("File not found"); + if (IsNetworkPath(filePath)) + throw new IOException("Network timeout occurred"); + } + + private static bool IsNetworkPath(string path) + { + return path.StartsWith(@"\\") || path.StartsWith("http"); + } + // + + // + public static void DemonstrateDebuggingAdvantage() + { + var contextData = new Dictionary + { + ["RequestId"] = Guid.NewGuid(), + ["UserId"] = "user123", + ["Timestamp"] = DateTime.Now + }; + + try + { + // Simulate a deep call stack. + Level1Method(contextData); + } + catch (Exception ex) when (LogAndFilter(ex, contextData)) + { + // This catch block may never execute if LogAndFilter returns false. + // But LogAndFilter can examine the exception while the stack is intact. + Console.WriteLine("Exception handled after logging"); + } + } + + private static void Level1Method(Dictionary context) + { + Level2Method(context); + } + + private static void Level2Method(Dictionary context) + { + Level3Method(context); + } + + private static void Level3Method(Dictionary context) + { + throw new InvalidOperationException("Error in deep call stack"); + } + + private static bool LogAndFilter(Exception ex, Dictionary context) + { + // This method runs before stack unwinding. + // Full call stack and local variables are still available. + Console.WriteLine($"Exception occurred: {ex.Message}"); + Console.WriteLine($"Request ID: {context["RequestId"]}"); + Console.WriteLine($"Full stack trace preserved: {ex.StackTrace}"); + + // Return true to handle the exception, false to continue search. + return ex.Message.Contains("deep call stack"); + } + // + } +} \ No newline at end of file diff --git a/docs/csharp/linq/standard-query-operators/index.md b/docs/csharp/linq/standard-query-operators/index.md index c3b452288f13a..f37870b4c9f46 100644 --- a/docs/csharp/linq/standard-query-operators/index.md +++ b/docs/csharp/linq/standard-query-operators/index.md @@ -89,7 +89,7 @@ The code produces the following XML output: For more information, see [Creating XML Trees in C# (LINQ to XML)](../../../standard/linq/create-xml-trees.md). -You can use the results of one query as the data source for a subsequent query. This example shows how to order the results of a join operation. This query creates a group join, and then sorts the groups based on the category element, which is still in scope. Inside the anonymous type initializer, a subquery orders all the matching elements from the products sequence. +You can use the results of one query as the data source for a subsequent query. This example shows how to order the results of a join operation. This query creates a group join, and then sorts the groups based on the `department` element, which is still in scope. Inside the anonymous type initializer, a subquery orders all the matching elements from the `students` sequence. :::code language="csharp" source="./snippets/standard-query-operators/OrderResultsOfJoin.cs" id="OrderResultsOfJoinQuery":::