From b2bd326ace411a28756f9f2e93e21ff289840a56 Mon Sep 17 00:00:00 2001 From: "azure-sdk-automation[bot]" <191533747+azure-sdk-automation[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:23:05 -0700 Subject: [PATCH 1/4] Update package index with latest published versions (#55002) Co-authored-by: azure-sdk --- docs/azure/includes/dotnet-all.md | 4 ++-- docs/azure/includes/dotnet-new.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/azure/includes/dotnet-all.md b/docs/azure/includes/dotnet-all.md index 574ccc41af79b..e1a71b749123f 100644 --- a/docs/azure/includes/dotnet-all.md +++ b/docs/azure/includes/dotnet-all.md @@ -104,8 +104,8 @@ | Schema Registry - Avro | NuGet [1.0.1](https://www.nuget.org/packages/Microsoft.Azure.Data.SchemaRegistry.ApacheAvro/1.0.1) | [docs](/dotnet/api/overview/azure/Microsoft.Azure.Data.SchemaRegistry.ApacheAvro-readme) | GitHub [1.0.1](https://github.com/Azure/azure-sdk-for-net/tree/Microsoft.Azure.Data.SchemaRegistry.ApacheAvro_1.0.1/sdk/schemaregistry/Microsoft.Azure.Data.SchemaRegistry.ApacheAvro/) | | Service Bus | NuGet [7.20.2](https://www.nuget.org/packages/Azure.Messaging.ServiceBus/7.20.2) | [docs](/dotnet/api/overview/azure/Messaging.ServiceBus-readme) | GitHub [7.20.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Messaging.ServiceBus_7.20.2/sdk/servicebus/Azure.Messaging.ServiceBus/) | | Storage - Blobs | NuGet [12.29.1](https://www.nuget.org/packages/Azure.Storage.Blobs/12.29.1)
NuGet [12.30.0-beta.1](https://www.nuget.org/packages/Azure.Storage.Blobs/12.30.0-beta.1) | [docs](/dotnet/api/overview/azure/Storage.Blobs-readme) | GitHub [12.29.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs_12.29.1/sdk/storage/Azure.Storage.Blobs/)
GitHub [12.30.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs_12.30.0-beta.1/sdk/storage/Azure.Storage.Blobs/) | -| Storage - Blobs Batch | NuGet [12.26.0](https://www.nuget.org/packages/Azure.Storage.Blobs.Batch/12.26.0) | [docs](/dotnet/api/overview/azure/Storage.Blobs.Batch-readme) | GitHub [12.26.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs.Batch_12.26.0/sdk/storage/Azure.Storage.Blobs.Batch/) | -| Storage - Blobs ChangeFeed | NuGet [12.0.0-preview.63](https://www.nuget.org/packages/Azure.Storage.Blobs.ChangeFeed/12.0.0-preview.63) | [docs](/dotnet/api/overview/azure/Storage.Blobs.ChangeFeed-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [12.0.0-preview.63](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs.ChangeFeed_12.0.0-preview.63/sdk/storage/Azure.Storage.Blobs.ChangeFeed/) | +| Storage - Blobs Batch | NuGet [12.26.0](https://www.nuget.org/packages/Azure.Storage.Blobs.Batch/12.26.0)
NuGet [12.27.0-beta.1](https://www.nuget.org/packages/Azure.Storage.Blobs.Batch/12.27.0-beta.1) | [docs](/dotnet/api/overview/azure/Storage.Blobs.Batch-readme) | GitHub [12.26.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs.Batch_12.26.0/sdk/storage/Azure.Storage.Blobs.Batch/)
GitHub [12.27.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs.Batch_12.27.0-beta.1/sdk/storage/Azure.Storage.Blobs.Batch/) | +| Storage - Blobs ChangeFeed | NuGet [12.0.0-preview.64](https://www.nuget.org/packages/Azure.Storage.Blobs.ChangeFeed/12.0.0-preview.64) | [docs](/dotnet/api/overview/azure/Storage.Blobs.ChangeFeed-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [12.0.0-preview.64](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs.ChangeFeed_12.0.0-preview.64/sdk/storage/Azure.Storage.Blobs.ChangeFeed/) | | Storage - Files Data Lake | NuGet [12.27.1](https://www.nuget.org/packages/Azure.Storage.Files.DataLake/12.27.1)
NuGet [12.28.0-beta.1](https://www.nuget.org/packages/Azure.Storage.Files.DataLake/12.28.0-beta.1) | [docs](/dotnet/api/overview/azure/Storage.Files.DataLake-readme) | GitHub [12.27.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Files.DataLake_12.27.1/sdk/storage/Azure.Storage.Files.DataLake/)
GitHub [12.28.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Files.DataLake_12.28.0-beta.1/sdk/storage/Azure.Storage.Files.DataLake/) | | Storage - Files Share | NuGet [12.27.1](https://www.nuget.org/packages/Azure.Storage.Files.Shares/12.27.1)
NuGet [12.28.0-beta.1](https://www.nuget.org/packages/Azure.Storage.Files.Shares/12.28.0-beta.1) | [docs](/dotnet/api/overview/azure/Storage.Files.Shares-readme) | GitHub [12.27.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Files.Shares_12.27.1/sdk/storage/Azure.Storage.Files.Shares/)
GitHub [12.28.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Files.Shares_12.28.0-beta.1/sdk/storage/Azure.Storage.Files.Shares/) | | Storage - Queues | NuGet [12.27.1](https://www.nuget.org/packages/Azure.Storage.Queues/12.27.1)
NuGet [12.28.0-beta.1](https://www.nuget.org/packages/Azure.Storage.Queues/12.28.0-beta.1) | [docs](/dotnet/api/overview/azure/Storage.Queues-readme) | GitHub [12.27.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Queues_12.27.1/sdk/storage/Azure.Storage.Queues/)
GitHub [12.28.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Queues_12.28.0-beta.1/sdk/storage/Azure.Storage.Queues/) | diff --git a/docs/azure/includes/dotnet-new.md b/docs/azure/includes/dotnet-new.md index 87920bb47ce97..08c8c466ce333 100644 --- a/docs/azure/includes/dotnet-new.md +++ b/docs/azure/includes/dotnet-new.md @@ -116,8 +116,8 @@ | Schema Registry - Avro | NuGet [1.0.1](https://www.nuget.org/packages/Microsoft.Azure.Data.SchemaRegistry.ApacheAvro/1.0.1) | [docs](/dotnet/api/overview/azure/Microsoft.Azure.Data.SchemaRegistry.ApacheAvro-readme) | GitHub [1.0.1](https://github.com/Azure/azure-sdk-for-net/tree/Microsoft.Azure.Data.SchemaRegistry.ApacheAvro_1.0.1/sdk/schemaregistry/Microsoft.Azure.Data.SchemaRegistry.ApacheAvro/) | | Service Bus | NuGet [7.20.2](https://www.nuget.org/packages/Azure.Messaging.ServiceBus/7.20.2) | [docs](/dotnet/api/overview/azure/Messaging.ServiceBus-readme) | GitHub [7.20.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Messaging.ServiceBus_7.20.2/sdk/servicebus/Azure.Messaging.ServiceBus/) | | Storage - Blobs | NuGet [12.29.1](https://www.nuget.org/packages/Azure.Storage.Blobs/12.29.1)
NuGet [12.30.0-beta.1](https://www.nuget.org/packages/Azure.Storage.Blobs/12.30.0-beta.1) | [docs](/dotnet/api/overview/azure/Storage.Blobs-readme) | GitHub [12.29.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs_12.29.1/sdk/storage/Azure.Storage.Blobs/)
GitHub [12.30.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs_12.30.0-beta.1/sdk/storage/Azure.Storage.Blobs/) | -| Storage - Blobs Batch | NuGet [12.26.0](https://www.nuget.org/packages/Azure.Storage.Blobs.Batch/12.26.0) | [docs](/dotnet/api/overview/azure/Storage.Blobs.Batch-readme) | GitHub [12.26.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs.Batch_12.26.0/sdk/storage/Azure.Storage.Blobs.Batch/) | -| Storage - Blobs ChangeFeed | NuGet [12.0.0-preview.63](https://www.nuget.org/packages/Azure.Storage.Blobs.ChangeFeed/12.0.0-preview.63) | [docs](/dotnet/api/overview/azure/Storage.Blobs.ChangeFeed-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [12.0.0-preview.63](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs.ChangeFeed_12.0.0-preview.63/sdk/storage/Azure.Storage.Blobs.ChangeFeed/) | +| Storage - Blobs Batch | NuGet [12.26.0](https://www.nuget.org/packages/Azure.Storage.Blobs.Batch/12.26.0)
NuGet [12.27.0-beta.1](https://www.nuget.org/packages/Azure.Storage.Blobs.Batch/12.27.0-beta.1) | [docs](/dotnet/api/overview/azure/Storage.Blobs.Batch-readme) | GitHub [12.26.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs.Batch_12.26.0/sdk/storage/Azure.Storage.Blobs.Batch/)
GitHub [12.27.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs.Batch_12.27.0-beta.1/sdk/storage/Azure.Storage.Blobs.Batch/) | +| Storage - Blobs ChangeFeed | NuGet [12.0.0-preview.64](https://www.nuget.org/packages/Azure.Storage.Blobs.ChangeFeed/12.0.0-preview.64) | [docs](/dotnet/api/overview/azure/Storage.Blobs.ChangeFeed-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [12.0.0-preview.64](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Blobs.ChangeFeed_12.0.0-preview.64/sdk/storage/Azure.Storage.Blobs.ChangeFeed/) | | Storage - Files Data Lake | NuGet [12.27.1](https://www.nuget.org/packages/Azure.Storage.Files.DataLake/12.27.1)
NuGet [12.28.0-beta.1](https://www.nuget.org/packages/Azure.Storage.Files.DataLake/12.28.0-beta.1) | [docs](/dotnet/api/overview/azure/Storage.Files.DataLake-readme) | GitHub [12.27.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Files.DataLake_12.27.1/sdk/storage/Azure.Storage.Files.DataLake/)
GitHub [12.28.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Files.DataLake_12.28.0-beta.1/sdk/storage/Azure.Storage.Files.DataLake/) | | Storage - Files Share | NuGet [12.27.1](https://www.nuget.org/packages/Azure.Storage.Files.Shares/12.27.1)
NuGet [12.28.0-beta.1](https://www.nuget.org/packages/Azure.Storage.Files.Shares/12.28.0-beta.1) | [docs](/dotnet/api/overview/azure/Storage.Files.Shares-readme) | GitHub [12.27.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Files.Shares_12.27.1/sdk/storage/Azure.Storage.Files.Shares/)
GitHub [12.28.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Files.Shares_12.28.0-beta.1/sdk/storage/Azure.Storage.Files.Shares/) | | Storage - Queues | NuGet [12.27.1](https://www.nuget.org/packages/Azure.Storage.Queues/12.27.1)
NuGet [12.28.0-beta.1](https://www.nuget.org/packages/Azure.Storage.Queues/12.28.0-beta.1) | [docs](/dotnet/api/overview/azure/Storage.Queues-readme) | GitHub [12.27.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Queues_12.27.1/sdk/storage/Azure.Storage.Queues/)
GitHub [12.28.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Storage.Queues_12.28.0-beta.1/sdk/storage/Azure.Storage.Queues/) | From 91314d1c68c0bf79b70c561164311a8bad5e20ef Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:05:56 +0200 Subject: [PATCH 2/4] Mention filename validation and permission handling for ZIP and TAR extraction (#54844) * Mention filename validation and permission handling for ZIP and TAR extraction * Feedback * Apply suggestions from code review Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> --------- Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> --- .../zip-tar-best-practices/csharp/Program.cs | 49 ++++++++++++++++++ docs/standard/io/zip-tar-best-practices.md | 51 ++++++++++++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs b/docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs index b2c750a9f0788..2cd9524693b12 100644 --- a/docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs +++ b/docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs @@ -71,6 +71,13 @@ void DangerousExtract(string extractDir) } // +bool ValidateName(string name) +{ + // Placeholder for a policy that checks for allowed characters, reserved names, etc. + // For example, you might disallow names with invalid characters or reserved device names. + return !string.IsNullOrWhiteSpace(name) && !name.Contains(".."); +} + // void SafeExtractZip(string archivePath, string destinationDir, long maxTotalSize, long maxEntrySize, int maxEntryCount) @@ -103,6 +110,19 @@ void SafeExtractZip(string archivePath, string destinationDir, if (totalSize > maxTotalSize) throw new InvalidOperationException("Archive exceeds total size limit."); + // The entry Name can contain arbitrary characters. Some characters might not be + // allowed on certain filesystems or have a special meaning. Applications should + // apply their own policies regarding allowed filenames. ValidateName is a placeholder + // for such a policy. + if (!ValidateName(entry.FullName)) + { + throw new IOException($"Entry name '{entry.FullName}' is not allowed."); + } + + // ExternalAttributes carry permission bits for Unix platforms. Clearing the + // attributes enforces extraction with default file permissions. + entry.ExternalAttributes = 0; + // Resolve the full destination path using Path.GetFullPath, which // normalizes away any "../" segments. Then verify the result still // starts with the destination directory. @@ -174,6 +194,35 @@ void SafeExtractTar(Stream archiveStream, string destinationDir, if (!allowedTypes.Contains(entry.EntryType)) continue; + // The entry Name can contain arbitrary characters. Some characters might not be + // allowed on certain filesystems or have a special meaning. Applications should + // apply their own policies regarding allowed filenames. ValidateName is a placeholder + // for such a policy. + if (!ValidateName(entry.Name)) + { + throw new IOException($"Entry name '{entry.Name}' is not allowed."); + } + + // Mask the entry's permission bits to a safe subset. + // This subset depends on your application's needs. + const UnixFileMode PermittedFileModes = + UnixFileMode.UserRead | UnixFileMode.UserWrite | + UnixFileMode.GroupRead | + UnixFileMode.OtherRead; + + const UnixFileMode PermittedDirectoryModes = + PermittedFileModes | + UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; + + if (entry.EntryType == TarEntryType.Directory) + { + entry.Mode &= PermittedDirectoryModes; + } + else + { + entry.Mode &= PermittedFileModes; + } + // Normalize and validate the path, same as the ZIP example. string destPath = Path.GetFullPath(Path.Join(fullDestDir, entry.Name)); if (!destPath.StartsWith(fullDestDir, StringComparison.Ordinal)) diff --git a/docs/standard/io/zip-tar-best-practices.md b/docs/standard/io/zip-tar-best-practices.md index 93c0beab80dfa..b7642267c3d17 100644 --- a/docs/standard/io/zip-tar-best-practices.md +++ b/docs/standard/io/zip-tar-best-practices.md @@ -84,8 +84,10 @@ For untrusted input—user uploads, third-party downloads, or network transfers - [What the convenience methods don't protect you from](#what-the-convenience-methods-dont-protect-you-from) - [Enforce size and entry count limits](#enforce-size-and-entry-count-limits) +- [Validate file names](#validate-file-names) - [Validate destination paths](#validate-destination-paths) - [Handle symbolic and hard links (TAR)](#handle-symbolic-and-hard-links-tar) +- [Entry permission bits (Unix only)](#entry-permission-bits-unix-only) - [Complete safe extraction examples](#complete-safe-extraction-examples) ### What the convenience methods don't protect you from @@ -115,6 +117,10 @@ Neither nor [!TIP] > The same approach applies to TAR archives. Since TAR files are read entry-by-entry via , track both the cumulative data size and entry count as you iterate. +### Validate file names + +Depending on the filesystem, some characters might not be allowed in filenames (or allowed only in certain positions), or have special meaning. Applications should check that the extracted entry names conform to an acceptable pattern. + ### Validate destination paths When you use the streaming APIs, you're responsible for validating every entry's destination path. They perform no path validation at all. @@ -162,6 +168,10 @@ If your use case requires extracting archives with hard links but you want to av For reference, validates both the entry path and link target path against the destination directory boundary. If either resolves outside, an is thrown. rejects symbolic and hard link entries entirely—it throws . +### Entry permission bits (Unix only) + +On Unix-like systems, the convenience APIs apply permission bits from the archive metadata to the extracted file or directory. These permissions might be too broad for the application scenario. In your application, you might want to prevent extraction of files that have executable permissions set. For more information, see the [Unix file permissions](#unix-file-permissions) section of this article. + ### Complete safe extraction examples Combine path traversal validation, size limits, entry count limits, and link handling in a single extraction loop. @@ -219,7 +229,44 @@ Archive behavior can vary between Windows and Unix. Keep these differences in mi - **ZIP:** Unix permissions are stored in the upper 16 bits of . When extracting on Unix via `ExtractToDirectory` or `ExtractToFile`, the runtime restores ownership permissions (read/write/execute for user/group/other), subject to the process umask. SetUID, SetGID, and StickyBit are stripped. Permissions are not applied if the upper bits are zero. This happens when the ZIP was created on Windows, because .NET on Windows sets `DefaultFileExternalAttributes` to `0`. On Windows, these attributes are always ignored during extraction. - **TAR:** The property represents `UnixFileMode` and can store all 12 permission bits (read/write/execute for user/group/other, plus SetUID, SetGID, and StickyBit). When extracting on Unix via `ExtractToDirectory` or `ExtractToFile`, the runtime applies only the 9 ownership bits (rwx for user/group/other), subject to the process umask. SetUID, SetGID, and StickyBit are stripped for security. -When processing untrusted archives, be aware that extracted files may have executable permissions set by the archive author. Untrusted archives could contain malicious executable files. +When processing untrusted archives, be aware that extracted files might have executable permissions set by the archive author. Untrusted archives could contain malicious executable files. Since and are writable, you can modify them before extraction: + +```csharp +foreach (ZipArchiveEntry entry in archive.Entries) +{ + // Unset the external attributes to force extraction with default Unix permission bits. + entry.ExternalAttributes = 0; + + // .. other validation omitted for brevity + + Directory.CreateDirectory(Path.GetDirectoryName(destPath)!); + entry.ExtractToFile(destPath, overwrite: false); +} +``` + +```csharp + +const UnixFileMode PermittedFileModes = + UnixFileMode.UserRead | UnixFileMode.UserWrite | + UnixFileMode.GroupRead | + UnixFileMode.OtherRead; + +const UnixFileMode PermittedDirectoryModes = + PermittedFileModes | + UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; + +while ((entry = reader.GetNextEntry()) is not null) +{ + if (entry.EntryType == TarEntryType.Directory) + { + entry.Mode &= PermittedDirectoryModes; + } + else + { + entry.Mode &= PermittedFileModes; + } +} +``` ### Special entry types (TAR) @@ -333,8 +380,10 @@ Before deploying code that handles archives from untrusted sources, verify you'v - **Manual iteration:** Don't use `ExtractToDirectory` for untrusted input—iterate entries manually to enforce all limits. - **Path traversal:** Validate all destination paths with `Path.GetFullPath()` + `StartsWith()`. +- **Special characters in names:** Validate that entry names conform to the application-defined policy. - **Decompression bombs:** Enforce limits on decompressed size (per-entry and total) and entry count. - **Symlink/hardlink attacks (TAR):** Validate link targets resolve within the destination, or skip link entries entirely. +- **Unix permissions:** Validate each entry's permission bits. Skip entries with too broad permissions or mask/modify the unwanted permission bits before extraction. - **Memory limits:** Avoid for large untrusted archives. Avoid mode with unseekable streams from untrusted sources. - **Thread safety:** Don't share , , or instances across threads. - **Untrusted metadata:** Treat entry names, comments, and extra fields as untrusted input. Sanitize before display or processing. From 1d30bf7e08e5b5666c17303e98f3481c685aa30a Mon Sep 17 00:00:00 2001 From: "azure-sdk-automation[bot]" <191533747+azure-sdk-automation[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:18:10 -0700 Subject: [PATCH 3/4] Update package index with latest published versions (#55005) Co-authored-by: azure-sdk --- docs/azure/includes/dotnet-all.md | 2 +- docs/azure/includes/dotnet-new.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/azure/includes/dotnet-all.md b/docs/azure/includes/dotnet-all.md index e1a71b749123f..e5ebdaf935097 100644 --- a/docs/azure/includes/dotnet-all.md +++ b/docs/azure/includes/dotnet-all.md @@ -114,7 +114,7 @@ | Synapse - Managed Private Endpoints | NuGet [1.0.0-beta.5](https://www.nuget.org/packages/Azure.Analytics.Synapse.ManagedPrivateEndpoints/1.0.0-beta.5) | [docs](/dotnet/api/overview/azure/Analytics.Synapse.ManagedPrivateEndpoints-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.5](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Analytics.Synapse.ManagedPrivateEndpoints_1.0.0-beta.5/sdk/synapse/Azure.Analytics.Synapse.ManagedPrivateEndpoints/) | | Synapse - Monitoring | NuGet [1.0.0-beta.3](https://www.nuget.org/packages/Azure.Analytics.Synapse.Monitoring/1.0.0-beta.3) | [docs](/dotnet/api/overview/azure/Analytics.Synapse.Monitoring-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Analytics.Synapse.Monitoring_1.0.0-beta.3/sdk/synapse/Azure.Analytics.Synapse.Monitoring/) | | Synapse - Spark | NuGet [1.0.0-preview.8](https://www.nuget.org/packages/Azure.Analytics.Synapse.Spark/1.0.0-preview.8) | [docs](/dotnet/api/overview/azure/Analytics.Synapse.Spark-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-preview.8](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Analytics.Synapse.Spark_1.0.0-preview.8/sdk/synapse/Azure.Analytics.Synapse.Spark/) | -| System Events | NuGet [1.0.0](https://www.nuget.org/packages/Azure.Messaging.EventGrid.SystemEvents/1.0.0)
NuGet [1.1.0-beta.1](https://www.nuget.org/packages/Azure.Messaging.EventGrid.SystemEvents/1.1.0-beta.1) | [docs](/dotnet/api/overview/azure/Messaging.EventGrid.SystemEvents-readme) | GitHub [1.0.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Messaging.EventGrid.SystemEvents_1.0.0/sdk/eventgrid/Azure.Messaging.EventGrid.SystemEvents/)
GitHub [1.1.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Messaging.EventGrid.SystemEvents_1.1.0-beta.1/sdk/eventgrid/Azure.Messaging.EventGrid.SystemEvents/) | +| System Events | NuGet [1.1.0](https://www.nuget.org/packages/Azure.Messaging.EventGrid.SystemEvents/1.1.0) | [docs](/dotnet/api/overview/azure/Messaging.EventGrid.SystemEvents-readme) | GitHub [1.1.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Messaging.EventGrid.SystemEvents_1.1.0/sdk/eventgrid/Azure.Messaging.EventGrid.SystemEvents/) | | System.ClientModel | NuGet [1.14.0](https://www.nuget.org/packages/System.ClientModel/1.14.0) | [docs](/dotnet/api/overview/azure/System.ClientModel-readme) | GitHub [1.14.0](https://github.com/Azure/azure-sdk-for-net/tree/System.ClientModel_1.14.0/sdk/core/System.ClientModel/) | | Tables | NuGet [12.11.0](https://www.nuget.org/packages/Azure.Data.Tables/12.11.0) | [docs](/dotnet/api/overview/azure/Data.Tables-readme) | GitHub [12.11.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Data.Tables_12.11.0/sdk/tables/Azure.Data.Tables/) | | Text Analytics | NuGet [5.3.0](https://www.nuget.org/packages/Azure.AI.TextAnalytics/5.3.0) | [docs](/dotnet/api/overview/azure/AI.TextAnalytics-readme) | GitHub [5.3.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.AI.TextAnalytics_5.3.0/sdk/textanalytics/Azure.AI.TextAnalytics/) | diff --git a/docs/azure/includes/dotnet-new.md b/docs/azure/includes/dotnet-new.md index 08c8c466ce333..b0089e930dfca 100644 --- a/docs/azure/includes/dotnet-new.md +++ b/docs/azure/includes/dotnet-new.md @@ -126,7 +126,7 @@ | Synapse - Managed Private Endpoints | NuGet [1.0.0-beta.5](https://www.nuget.org/packages/Azure.Analytics.Synapse.ManagedPrivateEndpoints/1.0.0-beta.5) | [docs](/dotnet/api/overview/azure/Analytics.Synapse.ManagedPrivateEndpoints-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.5](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Analytics.Synapse.ManagedPrivateEndpoints_1.0.0-beta.5/sdk/synapse/Azure.Analytics.Synapse.ManagedPrivateEndpoints/) | | Synapse - Monitoring | NuGet [1.0.0-beta.3](https://www.nuget.org/packages/Azure.Analytics.Synapse.Monitoring/1.0.0-beta.3) | [docs](/dotnet/api/overview/azure/Analytics.Synapse.Monitoring-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Analytics.Synapse.Monitoring_1.0.0-beta.3/sdk/synapse/Azure.Analytics.Synapse.Monitoring/) | | Synapse - Spark | NuGet [1.0.0-preview.8](https://www.nuget.org/packages/Azure.Analytics.Synapse.Spark/1.0.0-preview.8) | [docs](/dotnet/api/overview/azure/Analytics.Synapse.Spark-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-preview.8](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Analytics.Synapse.Spark_1.0.0-preview.8/sdk/synapse/Azure.Analytics.Synapse.Spark/) | -| System Events | NuGet [1.0.0](https://www.nuget.org/packages/Azure.Messaging.EventGrid.SystemEvents/1.0.0)
NuGet [1.1.0-beta.1](https://www.nuget.org/packages/Azure.Messaging.EventGrid.SystemEvents/1.1.0-beta.1) | [docs](/dotnet/api/overview/azure/Messaging.EventGrid.SystemEvents-readme) | GitHub [1.0.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Messaging.EventGrid.SystemEvents_1.0.0/sdk/eventgrid/Azure.Messaging.EventGrid.SystemEvents/)
GitHub [1.1.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Messaging.EventGrid.SystemEvents_1.1.0-beta.1/sdk/eventgrid/Azure.Messaging.EventGrid.SystemEvents/) | +| System Events | NuGet [1.1.0](https://www.nuget.org/packages/Azure.Messaging.EventGrid.SystemEvents/1.1.0) | [docs](/dotnet/api/overview/azure/Messaging.EventGrid.SystemEvents-readme) | GitHub [1.1.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Messaging.EventGrid.SystemEvents_1.1.0/sdk/eventgrid/Azure.Messaging.EventGrid.SystemEvents/) | | System.ClientModel | NuGet [1.14.0](https://www.nuget.org/packages/System.ClientModel/1.14.0) | [docs](/dotnet/api/overview/azure/System.ClientModel-readme) | GitHub [1.14.0](https://github.com/Azure/azure-sdk-for-net/tree/System.ClientModel_1.14.0/sdk/core/System.ClientModel/) | | Tables | NuGet [12.11.0](https://www.nuget.org/packages/Azure.Data.Tables/12.11.0) | [docs](/dotnet/api/overview/azure/Data.Tables-readme) | GitHub [12.11.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Data.Tables_12.11.0/sdk/tables/Azure.Data.Tables/) | | Text Analytics | NuGet [5.3.0](https://www.nuget.org/packages/Azure.AI.TextAnalytics/5.3.0) | [docs](/dotnet/api/overview/azure/AI.TextAnalytics-readme) | GitHub [5.3.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.AI.TextAnalytics_5.3.0/sdk/textanalytics/Azure.AI.TextAnalytics/) | From 13736d95e22387b316122c86e7d3a0ae823cfd75 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 23 Jul 2026 12:45:07 -0400 Subject: [PATCH 4/4] [Everyday C#] Fundamentals: Equality comparisons (#54849) * [Everyday C#] Phase E, PR 14b: Type system: equality Add fundamentals concept article on object equality for classes, structs, records, and tuples. Covers value equality vs. reference equality, Equals/==/ReferenceEquals semantics, IEquatable implementation pattern, and record compiler-generated equality. Backed by a net10.0 snippet project (0 warnings, 0 errors). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5dca83ef-a274-4737-96d5-a311bfe58550 * [Everyday C#] Move equality to expressions Relocate the Equality fundamentals article and snippets from Type system to the new Expressions folder, and update the TOC and relative links for the new location. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bc6b7895-3b18-45ff-99de-540618819cb3 * Address equality article review feedback Clarify default equality behavior, operator terminology, and manual value equality guidance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bc6b7895-3b18-45ff-99de-540618819cb3 * Tighten struct equality guidance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bc6b7895-3b18-45ff-99de-540618819cb3 * Clarify manual equality guidance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bc6b7895-3b18-45ff-99de-540618819cb3 * Deemphasize IEquatable Deemphasize the value of IEquatable throughout the article. * Deemphasize IEquatable in equality article Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bc6b7895-3b18-45ff-99de-540618819cb3 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Yet another editing pass Run another edit pass on this PR. * lint * Restructure for a better organization This organization of the material works much better. * Apply suggestions from code review Co-authored-by: Wade Pickett --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Wade Pickett Copilot-Session: 5dca83ef-a274-4737-96d5-a311bfe58550 Copilot-Session: bc6b7895-3b18-45ff-99de-540618819cb3 --- .../fundamentals/expressions/equality.md | 121 ++++++++++++++++++ .../expressions/snippets/equality/Program.cs | 105 +++++++++++++++ .../snippets/equality/equality.csproj | 10 ++ docs/csharp/toc.yml | 2 + 4 files changed, 238 insertions(+) create mode 100644 docs/csharp/fundamentals/expressions/equality.md create mode 100644 docs/csharp/fundamentals/expressions/snippets/equality/Program.cs create mode 100644 docs/csharp/fundamentals/expressions/snippets/equality/equality.csproj diff --git a/docs/csharp/fundamentals/expressions/equality.md b/docs/csharp/fundamentals/expressions/equality.md new file mode 100644 index 0000000000000..efbb6325469d7 --- /dev/null +++ b/docs/csharp/fundamentals/expressions/equality.md @@ -0,0 +1,121 @@ +--- +title: "C# Equality comparisons" +description: Learn how C# compares values and references with ==, !=, Equals, GetHashCode, and ReferenceEquals for classes, structs, records, and tuples. +ms.date: 07/22/2026 +ms.topic: concept-article +ai-usage: ai-assisted +--- + +# C# Equality comparisons + +> [!TIP] +> This article is part of the **Fundamentals** section for developers who already know at least one programming language and are learning C#. If you're new to programming, start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. +> +> **Coming from another language?** In Java, `==` on objects and JavaScript `===` on objects test identity, not content. C# classes work the same way by default. In Python, `==` calls `__eq__` and tests content by default , similar to how C# [records](../types/records.md) compare. C# [structs](../types/structs.md) also compare by value when you call `Equals`. + +C# distinguishes two kinds of equality. *Value equality* means two instances are equal when their data matches. *Reference equality* means two variables are equal only when they point to the same object in memory. This condition is also called *identity*. The kind of type gives you the best first clue about the default equality behavior: value types usually compare data, and reference types usually compare identity. Defaults aren't destiny, but that mental model prevents subtle bugs where two objects that look identical aren't considered equal, or where a mutation through one variable silently changes what another variable sees. + +## Value types, reference types, and equality defaults + +Every type in C# is either a *value type* or a *reference type*. A *value type* holds its data directly in the variable. A *reference type* holds a reference to an object. When you assign a reference-type variable to another variable, both variables refer to the same object. This article uses that distinction as a quick refresher. For more information about value types and reference types, see [Type system overview](../types/index.md#value-types-and-reference-types). + +The default equality behavior usually follows the kind of type: + +- **Built-in numeric types and [enums](../types/enums.md)** are value types. Two `int` variables are equal when their numeric values match. +- **[Structs](../types/structs.md)** are value types. A plain `struct` uses value equality when you call . +- **[Tuples](../types/tuples.md)** are value types. Two tuples are equal when all their element values match. +- **[Classes](../types/classes.md)** are reference types. A plain class uses reference equality, so `==` and test whether two variables point to the same object. + +A plain class shows reference equality. Two separate objects with the same data aren't equal, but two variables that refer to the same object are equal: + +:::code language="csharp" source="snippets/equality/Program.cs" ID="ClassEquality"::: + +A plain `struct` shows value equality through . Two struct instances are equal when their fields match: + +:::code language="csharp" source="snippets/equality/Program.cs" ID="StructEquality"::: + +Plain structs don't get a predefined `==` operator. Writing `p1 == p2` on a plain struct compiles only if the struct declares its own `operator ==`. If you need operator comparisons for a struct, define `==` and `!=` as a pair and keep them consistent with and . + +Tuples are value types too. Two tuples are equal when every element value matches. Element names in a named tuple are a compile-time convenience and aren't considered during comparison. Only positions and values matter: + +:::code language="csharp" source="snippets/equality/Program.cs" ID="TupleEquality"::: + +For more information about tuple syntax and deconstruction, see [Tuples and deconstruction](../types/tuples.md). + +## Types can define different equality semantics + +Defaults aren't destiny. Some types define equality semantics that differ from the type-kind default, and your own types can do the same when their data should determine equality. + +Common exceptions and customizations include: + +- **[Records](../types/records.md)** generate value equality and include `==`/`!=` operators. The next section shows how the `record` modifier gives value equality to both record classes and record structs. +- **Strings** are classes, but `==` and compare string content, not identity. +- **Your own classes and structs** can define value equality when their data should determine equality. + +Equality is woven through these related members: + +- `==`: the equality operator. Most types use this as the primary equality check. Its behavior depends on whether the type has a built-in or user-defined `==` operator. +- `!=`: the inequality operator. When a type defines a user-defined `==` operator, it must also define `!=`. +- : a virtual method inherited by every type. You can override it to change equality semantics for a type. +- : a virtual method used by hash-based collections. When two values are equal, their hash codes must also be equal. +- : a static method that always tests identity. + +## Use records for value equality + +Use the `record` modifier to give a data-focused type value equality when the type can be a record. The compiler generates , , and `==`/`!=` members that compare every declared property value. + +A `record class` is still a reference type, but it compares values instead of identity: + +:::code language="csharp" source="snippets/equality/Program.cs" ID="RecordEquality"::: + + confirms that `person1` and `person2` are different objects in memory, while `==` and return `True` because the compiler-generated equality compares property values. + +The same compiler generation applies to `record struct` types: + +:::code language="csharp" source="snippets/equality/Program.cs" ID="RecordStructEquality"::: + +Record types generate the whole equality set for their own type. Both `record class` and `record struct` types override and . They also generate `==` and `!=` operators, plus a typed `Equals` method for the record type. Unlike a plain `struct`, a `record struct` therefore supports `==` and `!=` automatically. For more information about record types and their equality semantics, see [Records](../types/records.md#value-equality). + +## Implement equality yourself when a type can't be a record + +> [!IMPORTANT] +> This section shows how to implement by hand the equality behavior that the compiler generates when you add `record` to a type. If your type can be a record, use `record` instead. It generates all these members for you. Implement them manually only when your type can't be a record. + +When a class or struct represents a value, such as a color or a measurement, the equality members for that type must agree. The easiest way to achieve this consistency is to declare the type as a `record`. If the type can't be a record, such as when it must derive from a non-record class, implement the equality members yourself. The language enforces that user-defined `==` and `!=` operators must be declared as a pair. If you provide those operators, compiler warning [CS0660](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) means the type also needs an override. Warning [CS0661](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) means the type also needs an override. + +In a complete manual implementation, provide these members: + +- `==` and `!=` operators. Add them as a pair because the compiler requires a type that overloads one to overload the other. +- An `override` of . This override changes equality semantics for the type and keeps object-level equality consistent. +- An `override` of . Objects that are equal must return the same hash code. Without this pairing, the type behaves incorrectly in hash-based collections such as `Dictionary` or `HashSet`. See for guidance on a correct implementation. +- Optionally, a typed `Equals` method by implementing . You often see this written as `Equals(T?)` in docs: `T` is a [type parameter](../types/generics.md), a placeholder for the current type, and `?` is a [nullable annotation](../null-safety/index.md) that says the argument can be `null`. This typed method can avoid extra conversions when callers already have the same type, but it's a secondary optimization. + +The following example starts with the and overrides, plus the optional typed `Equals` member, so you can see their effect before the `==` and `!=` operators are added. `HashCode.Combine` is a library helper that builds one hash code from the same values used by `Equals`: + +:::code language="csharp" source="snippets/equality/Program.cs" ID="ColorDefinition"::: + +At this point, `Equals` reflects value equality, but `==` still tests identity for the class because the type hasn't declared `==` and `!=` operators. Plain structs likewise still don't have a predefined `==` operator unless you declare one: + +:::code language="csharp" source="snippets/equality/Program.cs" ID="IEquatableUsage"::: + +Adding `==` and `!=` operators is the remaining step when you need operator comparisons. This article intentionally stops before the full operator implementation so the first pass can focus on the equality contract. The operator-focused follow-up shows the completed shape. For the operator syntax, see [Equality operators](../../language-reference/operators/equality-operators.md) in the language reference. + +## Use `Object.ReferenceEquals` to test identity directly + + always tests identity regardless of how a type overrides or overloads `==`. Use it as an identity diagnostic when you need to confirm whether two variables point to the exact same object: + +:::code language="csharp" source="snippets/equality/Program.cs" ID="ReferenceEqualsDemo"::: + +A common use is inside an `Equals` override to short-circuit the full comparison: when both arguments are the same reference, they're always equal without checking individual fields. + +> [!NOTE] +> Advanced detail: when variables are typed as an [interface](../types/interfaces.md), `==` checks whether the interface variables refer to the same object. A call to `Equals` still runs the underlying object's implementation. + +## See also + +- [Type system overview](../types/index.md) +- [Classes](../types/classes.md) +- [Structs](../types/structs.md) +- [Records](../types/records.md) +- [Tuples and deconstruction](../types/tuples.md) +- [Equality operators (language reference)](../../language-reference/operators/equality-operators.md) diff --git a/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs b/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs new file mode 100644 index 0000000000000..670765639cd50 --- /dev/null +++ b/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs @@ -0,0 +1,105 @@ +// +var order1 = new Order(42, "Shoes"); +var order2 = new Order(42, "Shoes"); + +Console.WriteLine(order1 == order2); // => False +Console.WriteLine(order1.Equals(order2)); // => False +Console.WriteLine(ReferenceEquals(order1, order2)); // => False + +Order order3 = order1; +Console.WriteLine(order1 == order3); // => True +// + +// +var pt1 = new Point(3, 4); +var pt2 = new Point(3, 4); + +Console.WriteLine(pt1.Equals(pt2)); // => True +// + +// +var person1 = new Person("Ada", "Lovelace"); +var person2 = new Person("Ada", "Lovelace"); + +Console.WriteLine(person1 == person2); // => True +Console.WriteLine(person1.Equals(person2)); // => True +Console.WriteLine(ReferenceEquals(person1, person2)); // => False +// + +// +var dim1 = new Dimension(1920, 1080); +var dim2 = new Dimension(1920, 1080); + +Console.WriteLine(dim1 == dim2); // => True +Console.WriteLine(dim1.Equals(dim2)); // => True +// + +// +var t1 = (Name: "Grace", Role: "Engineer"); +var t2 = (Name: "Grace", Role: "Engineer"); + +Console.WriteLine(t1 == t2); // => True +// + +// +var red1 = new Color(255, 0, 0); +var red2 = new Color(255, 0, 0); + +Console.WriteLine(red1.Equals(red2)); // => True +Console.WriteLine(red1 == red2); // => False (no == overload; identity check) +// + +// +var doc1 = new Document("Report"); +var doc2 = new Document("Report"); +var doc3 = doc1; + +Console.WriteLine(ReferenceEquals(doc1, doc2)); // => False +Console.WriteLine(ReferenceEquals(doc1, doc3)); // => True +// + +// ── Type declarations ──────────────────────────────────────────────────────── + +class Order(int id, string name) +{ + public int Id { get; } = id; + public string Name { get; } = name; +} + +struct Point(int x, int y) +{ + public int X { get; } = x; + public int Y { get; } = y; +} + +record Person(string First, string Last); + +record struct Dimension(double Width, double Height); + +// +class Color : IEquatable +{ + public Color(int r, int g, int b) + { + R = r; + G = g; + B = b; + } + + public int R { get; } + public int G { get; } + public int B { get; } + + public bool Equals(Color? other) => + other is not null && R == other.R && G == other.G && B == other.B; + + public override bool Equals(object? obj) => obj is Color other && Equals(other); + public override int GetHashCode() => HashCode.Combine(R, G, B); +} +// + +class Document(string title) +{ + public string Title { get; } = title; +} + diff --git a/docs/csharp/fundamentals/expressions/snippets/equality/equality.csproj b/docs/csharp/fundamentals/expressions/snippets/equality/equality.csproj new file mode 100644 index 0000000000000..dfb40caafcf9a --- /dev/null +++ b/docs/csharp/fundamentals/expressions/snippets/equality/equality.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index 511cb5c2df4a0..5284db7e654c5 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -115,6 +115,8 @@ items: href: fundamentals/tutorials/string-interpolation.md - name: Expressions and statements items: + - name: Equality + href: fundamentals/expressions/equality.md - name: Selection statements href: fundamentals/statements/selection.md - name: Iteration statements