diff --git a/docfx.json b/docfx.json index c7290f47f723c..8db5e8c761967 100644 --- a/docfx.json +++ b/docfx.json @@ -224,7 +224,7 @@ "docs/fundamentals/**/*.{md,yml}": "dotnet-fundamentals", "docs/machine-learning/**/*.{md,yml}": "dotnet-ml", "docs/standard/**/**.{md,yml}": "dotnet-fundamentals", - "docs/standard/data/**/**.{md,yml}": "dotnet-data", + "docs/standard/data/**/**.{md,yml}": "dotnet-data-access", "docs/standard/design-guidelines/*.{md,yml}": "dotnet", "docs/standard/security/*.{md,yml}": "dotnet", "docs/visual-basic/**/**.{md,yml}": "dotnet-visual-basic", diff --git a/docs/ai/conceptual/data-ingestion.md b/docs/ai/conceptual/data-ingestion.md index 0d0dab8a9f38a..747ecd8206f85 100644 --- a/docs/ai/conceptual/data-ingestion.md +++ b/docs/ai/conceptual/data-ingestion.md @@ -88,8 +88,7 @@ The writer can also automatically generate embeddings for your chunks using > embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); diff --git a/docs/ai/overview.md b/docs/ai/overview.md index 5482a1af78373..f4b921a55878d 100644 --- a/docs/ai/overview.md +++ b/docs/ai/overview.md @@ -24,11 +24,10 @@ Millions of developers use .NET to create applications that run on the web, on m .NET libraries support a wide range of AI service providers, enabling you to build applications with the AI platform that best fits your needs. The following table lists the major AI providers that integrate with `Microsoft.Extensions.AI`: | Provider | Description | -|----------|------------------------|---------------------------|-----------------|-------------| +|----------|------------------| | OpenAI | Direct integration with OpenAI's models including GPT-4, GPT-3.5, and DALL-E | | Azure OpenAI | Enterprise-grade OpenAI models hosted on Azure with enhanced security and compliance | | Azure AI Foundry | Microsoft's managed platform for building and deploying AI agents at scale | -| GitHub Models | Access to models available through GitHub's AI model marketplace | | Ollama | Run open-source models locally, for example, Llama, Mistral, and Phi-3 | | Google Gemini | Google's multimodal AI models | | Amazon Bedrock | AWS's managed service for foundation models | diff --git a/docs/ai/quickstarts/ai-templates.md b/docs/ai/quickstarts/ai-templates.md index c3d33508a2e49..e835bf2e27ae6 100644 --- a/docs/ai/quickstarts/ai-templates.md +++ b/docs/ai/quickstarts/ai-templates.md @@ -11,12 +11,6 @@ ai-usage: ai-assisted In this quickstart, you learn how to create a .NET AI app to chat with custom data using the .NET AI app template. The template is designed to streamline the getting started experience for building AI apps with .NET by handling common setup tasks and configurations for you. -:::zone target="docs" pivot="github-models" - -[!INCLUDE [ai-templates-github-models](includes/ai-templates-github-models.md)] - -:::zone-end - :::zone target="docs" pivot="azure-openai" [!INCLUDE [ai-templates-azure-openai](includes/ai-templates-azure-openai.md)] diff --git a/docs/ai/quickstarts/includes/ai-templates-github-models.md b/docs/ai/quickstarts/includes/ai-templates-github-models.md deleted file mode 100644 index b477bcf3514b2..0000000000000 --- a/docs/ai/quickstarts/includes/ai-templates-github-models.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -ms.date: 2/21/2025 -ms.topic: include ---- - -## Prerequisites - -* [.NET 9.0 SDK](https://dotnet.microsoft.com/download) -* One of the following IDEs (optional): - * [Visual Studio](https://visualstudio.microsoft.com/) - * [Visual Studio Code](https://code.visualstudio.com) with [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) - -## Install the .NET AI app template - -The **AI Chat Web App** template is available as a template package through NuGet. Use the [`dotnet new install`](../../../core/tools/dotnet-new-install.md) command to install the package: - -```dotnetcli -dotnet new install Microsoft.Extensions.AI.Templates -``` - -## Create the .NET AI app - -After you install the AI app templates, you can use them to create starter apps through Visual Studio UI, Visual Studio Code, or the .NET CLI. - -# [Visual Studio](#tab/visual-studio) - -1. Inside Visual Studio, navigate to **File > New > Project**. -1. On the **Create a new project** screen, search for **AI Chat Web App**. Select the matching result and then choose **Next**. -1. On the **Configure your new project** screen, enter the desired name and location for your project and then choose **Next**. -1. On the **Additional information** screen: - - For the **Framework** option, select **.NET 9.0**. - - For the **AI service provider** option, select **GitHub Models**. - - For the **Vector store** option, select **Local on-disc (for prototyping)**. -1. Select **Create** to complete the process. - -# [Visual Studio Code](#tab/visual-studio-code) - -1. Open the command palette in Visual Studio Code. -1. Search for *New project* and select the result called **.NET: New Project**. -1. Filter the project templates list by searching for *AI*. -1. Select **AI Chat Web App** and press Enter. - -> [!NOTE] -> The command palette experience currently only supports the default settings. To configure your AI platform and vector store during template creation, use the Visual Studio or .NET CLI workflows. - -# [.NET CLI](#tab/dotnet-cli) - -1. In a terminal window, navigate to an empty directory on your device. -1. Create a new app with the `dotnet new` command and the following parameters: - - ```dotnetcli - dotnet new aichatweb --Framework net9.0 --provider githubmodels --vector-store local - ``` - - The .NET CLI creates a new .NET 9.0 app with the configurations you specified. - -1. Open the new app in your editor of choice, such as Visual Studio Code. - - ```dotnetcli - code . - ``` - ---- - -### Explore the sample app - -The sample app you created is a Blazor Interactive Server web app preconfigured with common AI and data services. The app handles the following concerns for you: - -- Includes essential `Microsoft.Extensions.AI` packages and other dependencies in the `csproj` file to help you get started working with AI. -- Creates various AI services and registers them for dependency injection in the `Program.cs` file: - - An `IChatClient` service to chat back and forth with the generative AI model - - An `IEmbeddingGenerator` service that's used to generate embeddings, which are essential for vector search functionality - - A `JsonVectorStore` to act as an in-memory vector store -- Registers a SQLite database context service to handle ingesting documents. The app is preconfigured to ingest whatever documents you add to the `Data` folder of the project, including the provided example files. -- Provides a complete chat UI using Blazor components. The UI handles rich formatting for the AI responses and provides features such as citations for response data. - -## Configure access to GitHub Models - -To authenticate to GitHub models from your code, you'll need to [create a GitHub personal access token](https://docs.github.com/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token): - -1. Navigate to the **Personal access tokens** page of your GitHub account settings under **Developer Settings**. -1. Select **Generate new token**. -1. Enter a name for the token, and under **Permissions**, set **Models** to **Access: Read-only**. -1. Select **Generate token** at the bottom of the page. -1. Copy the token for use in the steps ahead. - -## Configure the app - -The **AI Chat Web App** is almost ready to go as soon as it's created. However, you need to configure the app to use the personal access token you set up for GitHub Models. By default, the app template searches for this value in the project's local .NET user secrets. You can manage user secrets using either the Visual Studio UI or the .NET CLI. - -> [!NOTE] -> If you enabled Aspire for your app, skip to the [Aspire configuration](#aspire-configuration) section. - -## [Visual Studio](#tab/configure-visual-studio) - -1. Right-click on your project in the Solution Explorer and select **Manage User Secrets**. This opens a `secrets.json` file where you can store your API keys without them being tracked in source control. - -2. Add the following key and value: - - ```json - { - "GitHubModels:Token": "" - } - ``` - -## [.NET CLI](#tab/configure-dotnet-cli) - -1. Open a terminal window set to the root of your project. - -1. Run the `dotnet user-secrets set` command to set the user secret: - - ```dotnetcli - dotnet user-secrets set GitHubModels:Token - ``` - ---- - -### Aspire configuration - -To use the **AI Chat Web App** template with Aspire orchestration, add the following configurations: - -#### [Visual Studio](#tab/configure-visual-studio-aspire) - -1. Right-click on your `*.AppHost` project in the Solution Explorer and select **Manage User Secrets**. This opens a `secrets.json` file where you can store your API keys without them being tracked in source control. - -2. Add the following key and value: - - ```json - { - "ConnectionStrings:openai": "Endpoint=https://models.inference.ai.azure.com;Key=" - } - ``` - -#### [.NET CLI](#tab/configure-dotnet-cli-aspire) - -1. Open a terminal window set to the root of your `*.AppHost` project. - -1. Run the `dotnet user-secrets set` command to set the user secret: - - ```dotnetcli - dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://models.inference.ai.azure.com;Key=" - ``` - ---- - -By default, the app template uses the `gpt-4o-mini` and `text-embedding-3-small` models. To try other models, update the name parameters in `Program.cs`: - - ```csharp - var chatClient = ghModelsClient.AsChatClient("gpt-4o-mini"); - var embeddingGenerator = ghModelsClient.AsEmbeddingGenerator("text-embedding-3-small"); - ``` diff --git a/docs/azure/includes/dotnet-all.md b/docs/azure/includes/dotnet-all.md index ae4926a9eec8c..cf7bac980e943 100644 --- a/docs/azure/includes/dotnet-all.md +++ b/docs/azure/includes/dotnet-all.md @@ -629,7 +629,7 @@ | Functions Extensions - Cosmos DB ChangeProcessor | NuGet [1.0.2](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.CosmosDb.ChangeProcessor/1.0.2) | | | | Functions Extensions - Dapr | NuGet [1.0.1](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.Dapr/1.0.1) | | | | Functions Extensions - Dapr Core | NuGet [1.0.1](https://www.nuget.org/packages/Microsoft.Azure.Functions.Extensions.Dapr.Core/1.0.1) | | | -| Functions Extensions - HTTP AspNet Core | NuGet [2.1.0](https://www.nuget.org/packages/Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore/2.1.0) | | | +| Functions Extensions - HTTP AspNet Core | NuGet [2.1.1](https://www.nuget.org/packages/Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore/2.1.1) | | | | Functions Extensions - Redis | NuGet [1.0.0](https://www.nuget.org/packages/Microsoft.Azure.Functions.Worker.Extensions.Redis/1.0.0) | | | | Functions Extensions - Redis | NuGet [1.0.0](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.Redis/1.0.0) | | | | Functions Extensions - Worker Extentions | NuGet [1.0.1](https://www.nuget.org/packages/Microsoft.Azure.Functions.Worker.Extensions.Dapr/1.0.1) | | | diff --git a/docs/core/testing/microsoft-testing-platform-architecture-extensions.md b/docs/core/testing/microsoft-testing-platform-architecture-extensions.md index aa32c4872d13e..98ed287d3863b 100644 --- a/docs/core/testing/microsoft-testing-platform-architecture-extensions.md +++ b/docs/core/testing/microsoft-testing-platform-architecture-extensions.md @@ -3,7 +3,7 @@ title: Build extensions for Microsoft.Testing.Platform (MTP) description: Learn how to create in-process and out-of-process extensions for Microsoft.Testing.Platform (MTP). author: MarcoRossignoli ms.author: mrossignoli -ms.date: 06/16/2026 +ms.date: 07/17/2026 ai-usage: ai-assisted --- @@ -150,9 +150,9 @@ Please note that the `ValidateCommandLineOptionsAsync` method provides the [`ICo ### The `ITestSessionLifetimeHandler` extensions -The `ITestSessionLifeTimeHandler` is an *in-process* extension that enables the execution of code *before* and *after* the test session. +The `ITestSessionLifetimeHandler` is an *in-process* extension that enables the execution of code *before* and *after* the test session. -To register a custom `ITestSessionLifeTimeHandler`, utilize the following API: +To register a custom `ITestSessionLifetimeHandler`, use the following API: ```csharp var builder = await TestApplication.CreateBuilderAsync(args); @@ -160,7 +160,7 @@ var builder = await TestApplication.CreateBuilderAsync(args); // ... builder.TestHost.AddTestSessionLifetimeHandle( - static serviceProvider => new CustomTestSessionLifeTimeHandler()); + static serviceProvider => new CustomTestSessionLifetimeHandler()); ``` The factory utilizes the [IServiceProvider](./microsoft-testing-platform-architecture-services.md#the-imessagebus-service) to gain access to the suite of services offered by the testing platform. @@ -168,18 +168,21 @@ The factory utilizes the [IServiceProvider](./microsoft-testing-platform-archite > [!IMPORTANT] > The sequence of registration is significant, as the APIs are called in the order they were registered. -The `ITestSessionLifeTimeHandler` interface includes the following methods: +The `ITestSessionLifetimeHandler` interface includes the following methods: ```csharp public interface ITestSessionLifetimeHandler : ITestHostExtension { - Task OnTestSessionStartingAsync( - SessionUid sessionUid, - CancellationToken cancellationToken); + Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext); - Task OnTestSessionFinishingAsync( - SessionUid sessionUid, - CancellationToken cancellationToken); + Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext); +} + +public interface ITestSessionContext +{ + SessionUid SessionUid { get; } + + CancellationToken CancellationToken { get; } } public readonly struct SessionUid(string value) @@ -192,15 +195,18 @@ public interface ITestHostExtension : IExtension } ``` +> [!IMPORTANT] +> In MTP 2.0.0, both methods changed to take a single `ITestSessionContext` parameter that exposes the `SessionUid` and the `CancellationToken`. In MTP 1.x, each method took a separate `SessionUid` and `CancellationToken` argument. For more information, see [Migrate from Microsoft.Testing.Platform (MTP) v1 to v2](microsoft-testing-platform-migration-from-v1-to-v2.md#api-signature-changes). + The `ITestSessionLifetimeHandler` is a type of `ITestHostExtension`, which serves as a base for all *test host* extensions. Like all other extension points, it also inherits from [IExtension](./microsoft-testing-platform-architecture-test-framework.md#the-iextension-interface). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. Consider the following details for this API: -`OnTestSessionStartingAsync`: This method is invoked prior to the commencement of the test session and receives the `SessionUid` object, which provides an opaque identifier for the current test session. +`OnTestSessionStartingAsync`: This method is invoked prior to the commencement of the test session and receives the `ITestSessionContext`, whose `SessionUid` provides an opaque identifier for the current test session. `OnTestSessionFinishingAsync`: This method is invoked after the completion of the test session, ensuring that the [testing framework](./microsoft-testing-platform-architecture-test-framework.md#test-framework-extension) has finished executing all tests and has reported all relevant data to the platform. Typically, in this method, the extension employs the [`IMessageBus`](./microsoft-testing-platform-architecture-services.md#the-imessagebus-service) to transmit custom assets or data to the shared platform bus. This method can also signal to any custom *out-of-process* extension that the test session has concluded. -Finally, both APIs take a `CancellationToken` which the extension is expected to honor. +Finally, the `ITestSessionContext` exposes a `CancellationToken` that the extension is expected to honor. If your extension requires intensive initialization and you need to use the async/await pattern, you can refer to the [`Async extension initialization and cleanup`](#asynchronous-initialization-and-cleanup-of-extensions). If you need to *share state* between extension points, you can refer to the [`CompositeExtensionFactory`](#the-compositeextensionfactoryt) section. @@ -257,7 +263,7 @@ Finally, both APIs take a `CancellationToken` which the extension is expected to ### The `IDataConsumer` extensions -The `IDataConsumer` is an *in-process* extension capable of subscribing to and receiving `IData` information that is pushed to the [IMessageBus](./microsoft-testing-platform-architecture-services.md#the-imessagebus-service) by the [testing framework](./microsoft-testing-platform-architecture-test-framework.md#test-framework-extension) and its extensions. +The `IDataConsumer` is an *in-process* extension capable of subscribing to and receiving `IData` information that is published to the [IMessageBus](./microsoft-testing-platform-architecture-services.md#the-imessagebus-service) by the [testing framework](./microsoft-testing-platform-architecture-test-framework.md#test-framework-extension) and its extensions. *This extension point is crucial as it enables developers to gather and process all the information generated during a test session.* @@ -280,7 +286,7 @@ The factory utilizes the [IServiceProvider](./microsoft-testing-platform-archite The `IDataConsumer` interface includes the following methods: ```csharp -public interface IDataConsumer : ITestHostExtension +public interface IDataConsumer : IExtension { Type[] DataTypesConsumed { get; } @@ -297,11 +303,14 @@ public interface IData } ``` -The `IDataConsumer` is a type of `ITestHostExtension`, which serves as a base for all *test host* extensions. Like all other extension points, it also inherits from [IExtension](./microsoft-testing-platform-architecture-test-framework.md#the-iextension-interface). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. +> [!IMPORTANT] +> In MTP 2.0.0, `IDataConsumer` moved to the `Microsoft.Testing.Platform.Extensions` namespace and now extends [`IExtension`](./microsoft-testing-platform-architecture-test-framework.md#the-iextension-interface) directly. In MTP 1.x, it extended `ITestHostExtension`. You still register it with `builder.TestHost.AddDataConsumer(...)`. For more information, see [Migrate from Microsoft.Testing.Platform (MTP) v1 to v2](microsoft-testing-platform-migration-from-v1-to-v2.md#api-signature-changes). + +The `IDataConsumer` inherits from [IExtension](./microsoft-testing-platform-architecture-test-framework.md#the-iextension-interface). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. `DataTypesConsumed`: This property returns a list of `Type` that this extension plans to consume. It corresponds to `IDataProducer.DataTypesProduced`. Notably, an `IDataConsumer` can subscribe to multiple types originating from different `IDataProducer` instances without any issues. -`ConsumeAsync`: This method is triggered whenever data of a type to which the current consumer is subscribed is pushed onto the [`IMessageBus`](./microsoft-testing-platform-architecture-services.md#the-imessagebus-service). It receives the `IDataProducer` to provide details about the data payload's producer, as well as the `IData` payload itself. As you can see, `IData` is a generic placeholder interface that contains general informative data. The ability to push different types of `IData` implies that the consumer needs to *switch* on the type itself to cast it to the correct type and access the specific information. +`ConsumeAsync`: This method is triggered whenever data of a type to which the current consumer is subscribed is published to the [`IMessageBus`](./microsoft-testing-platform-architecture-services.md#the-imessagebus-service). It receives the `IDataProducer` to provide details about the data payload's producer, as well as the `IData` payload itself. As you can see, `IData` is a generic placeholder interface that contains general informative data. The ability to publish different types of `IData` implies that the consumer needs to *switch* on the type itself to cast it to the correct type and access the specific information. A sample implementation of a consumer that wants to elaborate the [`TestNodeUpdateMessage`](./microsoft-testing-platform-architecture-test-framework.md#the-testnodeupdatemessage-data) produced by a [testing framework](./microsoft-testing-platform-architecture-test-framework.md#test-framework-extension) could be: @@ -351,15 +360,106 @@ internal class CustomDataConsumer : IDataConsumer, IOutputDeviceDataProducer Finally, the API takes a `CancellationToken` which the extension is expected to honor. > [!IMPORTANT] -> It's crucial to process the payload directly within the `ConsumeAsync` method. The [IMessageBus](./microsoft-testing-platform-architecture-services.md#the-imessagebus-service) can manage both synchronous and asynchronous processing, coordinating the execution with the [testing framework](./microsoft-testing-platform-architecture-test-framework.md#test-framework-extension). Although the consumption process is entirely asynchronous and doesn't block the [IMessageBus.Push](./microsoft-testing-platform-architecture-services.md#the-imessagebus-service) at the time of writing, this is an implementation detail that may change in the future due to future requirements. However, the platform ensures that this method is always called once, eliminating the need for complex synchronization, as well as managing the scalability of the consumers. +> Process the payload directly within the `ConsumeAsync` method. A regular `IDataConsumer` consumes data asynchronously: the [IMessageBus](./microsoft-testing-platform-architecture-services.md#the-imessagebus-service) queues each published payload and processes it on a background loop, so `IMessageBus.PublishAsync` doesn't block the producer, and there's no guarantee about when `ConsumeAsync` runs relative to the producer continuing its work. The platform serializes delivery so that only one payload is processed at a time per consumer, which eliminates the need for complex synchronization within a single consumer. + + + +> [!NOTE] +> For scenarios that need a guarantee that consumption happens before the producer proceeds (for example, before a test starts running), MTP 2.3.0 introduced the experimental `IBlockingDataConsumer` marker interface (requires the `TPEXP` diagnostic to be suppressed). A consumer that also implements `IBlockingDataConsumer` is invoked *inline* by the message bus: calls are serialized, `PublishAsync` blocks until `ConsumeAsync` completes, and any exception thrown by `ConsumeAsync` propagates back to the producer that published the data. The message bus skips delivering a producer's data back to that same producer (same UID), so publishing under your own UID is safe. However, a blocking consumer must not, from within `ConsumeAsync`, publish data that is routed back to itself under a *different* producer UID, because that reentrancy would deadlock. > [!WARNING] -> When using `IDataConsumer` in conjunction with [ITestHostProcessLifetimeHandler](#the-itestsessionlifetimehandler-extensions) within a [composite extension point](#the-compositeextensionfactoryt), **it's crucial to disregard any data received post the execution of [ITestSessionLifetimeHandler.OnTestSessionFinishingAsync](#the-itestsessionlifetimehandler-extensions)**. The `OnTestSessionFinishingAsync` is the final opportunity to process accumulated data and transmit new information to the [IMessageBus](./microsoft-testing-platform-architecture-services.md#the-imessagebus-service), hence, any data consumed beyond this point will not be *utilizable* by the extension. +> When using `IDataConsumer` in conjunction with [ITestSessionLifetimeHandler](#the-itestsessionlifetimehandler-extensions) within a [composite extension point](#the-compositeextensionfactoryt), **it's crucial to disregard any data received post the execution of [ITestSessionLifetimeHandler.OnTestSessionFinishingAsync](#the-itestsessionlifetimehandler-extensions)**. The `OnTestSessionFinishingAsync` is the final opportunity to process accumulated data and transmit new information to the [IMessageBus](./microsoft-testing-platform-architecture-services.md#the-imessagebus-service), hence, any data consumed beyond this point will not be *utilizable* by the extension. If your extension requires intensive initialization and you need to use the async/await pattern, you can refer to the [`Async extension initialization and cleanup`](#asynchronous-initialization-and-cleanup-of-extensions). If you need to *share state* between extension points, you can refer to the [`CompositeExtensionFactory`](#the-compositeextensionfactoryt) section. +### Message bus file artifacts + +A custom `IData` payload has no automatic UI or command-line output. The platform only surfaces data for which a matching consumer is registered, so if you publish your own `IData` type and nothing consumes it, nothing is printed or forwarded. To make files that your extension produces visible to users and tooling, publish one of the built-in file artifact messages, which the built-in *terminal* and *dotnet test* consumers already recognize. + +For run-level or session-level files, publish a `FileArtifact` or a `SessionFileArtifact`. Both were introduced in MTP 1.0.0 and live in the `Microsoft.Testing.Platform.Extensions.Messages` namespace: + +- `FileArtifact` is unscoped. Use it for a file that isn't tied to a specific test session. +- `SessionFileArtifact` is scoped to a run/session through its `SessionUid`. Use it for artifacts produced for the run as a whole, such as coverage results, reports, dumps, or recorded videos. + +The built-in terminal and `dotnet test` consumers pick up both types and print or forward the file paths, so the files become discoverable in the console output and across the `dotnet test` pipe. Because consumers control the ultimate presentation, keep each file on disk and available for as long as it might be consumed or forwarded; don't delete it inside the same `PublishAsync` call. + +The artifact object itself carries no producer identity. The message bus supplies the originating `IDataProducer` to each consumer through the `dataProducer` argument of `IDataConsumer.ConsumeAsync`, so consumers learn who produced a file from that argument, not from the artifact. The message bus also doesn't take ownership of, move, or delete the referenced file: the producer owns the file's lifetime, and consumers only receive, read, or forward its path. + +> [!NOTE] +> Third-party `IDataConsumer` registration is public only on `builder.TestHost` (the *in-process* test host). There's no public `builder.TestHostControllers.AddDataConsumer` API for *out-of-process* consumers. The first-party report extensions that display artifacts use internal platform integration that custom extensions can't rely on. To surface files from your own extension, publish the built-in artifact messages described here and let the built-in consumers present them. + +A producer that publishes a session artifact must implement `IDataProducer` and list the exact runtime message types it publishes in `DataTypesProduced`. The following example publishes a coverage report when the session finishes: + +```csharp +internal sealed class CoverageReportProducer(IMessageBus messageBus) + : IDataProducer, ITestSessionLifetimeHandler +{ + public string Uid => nameof(CoverageReportProducer); + public string Version => "1.0.0"; + public string DisplayName => "Coverage report producer"; + public string Description => "Publishes the coverage report as a session artifact."; + + // List the exact runtime message types this producer publishes. + public Type[] DataTypesProduced => new[] { typeof(SessionFileArtifact) }; + + public Task IsEnabledAsync() => Task.FromResult(true); + + public Task OnTestSessionStartingAsync(ITestSessionContext context) + => Task.CompletedTask; + + public Task OnTestSessionFinishingAsync(ITestSessionContext context) + { + var report = new FileInfo("coverage.cobertura.xml"); + return messageBus.PublishAsync( + this, + new SessionFileArtifact( + context.SessionUid, + report, + "Code coverage", + "Cobertura coverage report for the run.")); + } +} +``` + +To attach a file to a *specific test* so that the terminal, `dotnet test`, and IDEs associate and display it with that test, don't publish a standalone file artifact. Instead, add one or more `FileArtifactProperty` entries to the `TestNode` that your [testing framework](./microsoft-testing-platform-architecture-test-framework.md#test-framework-extension) reports through a [`TestNodeUpdateMessage`](./microsoft-testing-platform-architecture-test-framework.md#the-testnodeupdatemessage-data). `FileArtifactProperty` was introduced in MTP 1.7.0: + +```csharp +var testNode = new TestNode +{ + Uid = testUid, + DisplayName = testDisplayName, + Properties = new PropertyBag( + PassedTestNodeStateProperty.CachedInstance, + new FileArtifactProperty( + new FileInfo("screenshot.png"), + "Failure screenshot", + "Screenshot captured while the test ran.")), +}; + +await messageBus.PublishAsync( + dataProducer, + new TestNodeUpdateMessage(sessionUid, testNode)); +``` + +> [!IMPORTANT] +> `TestNodeFileArtifact` is obsolete and was removed in MTP 2.0.0. To attach test-level files, use `FileArtifactProperty` on the `TestNode`. For more information, see [Migrate from Microsoft.Testing.Platform (MTP) v1 to v2](microsoft-testing-platform-migration-from-v1-to-v2.md#removed-obsolete-types). + +> [!NOTE] +> MTP 2.4.0 (unreleased as of July 2026) adds an experimental `kind` constructor overload and `Kind` property to `FileArtifact` and `SessionFileArtifact` (requires the `TPEXP` diagnostic to be suppressed). `Kind` is a producer-asserted, reverse-DNS identifier of the artifact *format* (for example, `microsoft.testing.trx`, `microsoft.testing.junit`, `microsoft.testing.ctrf`, or `microsoft.testing.html`) that post-processing can use to group artifacts of the same format for consolidation. Leave it `null` or omit it when the producer doesn't declare a known kind. At this time, `Kind` is only a metadata contract; don't assume broad merge orchestration beyond that. +> +> ```csharp +> #pragma warning disable TPEXP // Experimental API. +> new SessionFileArtifact( +> context.SessionUid, +> trxFile, +> "TRX report", +> "Test results in TRX format.", +> kind: "microsoft.testing.trx"); +> #pragma warning restore TPEXP +> ``` + ### The `ITestHostEnvironmentVariableProvider` extensions The `ITestHostEnvironmentVariableProvider` is an *out-of-process* extension that enables you to establish custom environment variables for the test host. Utilizing this extension point ensures that the testing platform will initiate a new host with the appropriate environment variables, as detailed in the [architecture](./microsoft-testing-platform-architecture.md) section. @@ -687,3 +787,6 @@ The following combinations are possible: * For `ITestApplicationBuilder.TestHost`, you can combine `IDataConsumer` and `ITestSessionLifetimeHandler`. * For `ITestApplicationBuilder.TestHostControllers`, you can combine `ITestHostEnvironmentVariableProvider` and `ITestHostProcessLifetimeHandler`. + +> [!NOTE] +> `IDataConsumer` is an *in-process* extension, so custom consumers are registered only through `builder.TestHost` (including via `CompositeExtensionFactory`). There's no public API to register an `IDataConsumer` on `builder.TestHostControllers`. diff --git a/docs/core/testing/microsoft-testing-platform-architecture-test-framework.md b/docs/core/testing/microsoft-testing-platform-architecture-test-framework.md index db9e93809a210..6a9ffb12b45cf 100644 --- a/docs/core/testing/microsoft-testing-platform-architecture-test-framework.md +++ b/docs/core/testing/microsoft-testing-platform-architecture-test-framework.md @@ -3,7 +3,7 @@ title: Build a test framework for Microsoft.Testing.Platform (MTP) description: Learn how to create a custom test framework for Microsoft.Testing.Platform (MTP), including registration, lifecycle, and test node reporting. author: MarcoRossignoli ms.author: mrossignoli -ms.date: 02/24/2026 +ms.date: 07/17/2026 ai-usage: ai-assisted --- @@ -390,7 +390,12 @@ internal sealed class TestingFramework } ``` -If your test adapter requires the publication of *files* during execution, you can find the recognized properties in this source file: . As you can see, you can provide file assets in a general manner or associate them with a specific `TestNode`. Remember, if you intend to push a `SessionFileArtifact`, you must declare it to the platform in advance, as shown below: +If your test adapter produces *files* during execution, the platform recognizes two distinct mechanisms, and the right one depends on whether the file belongs to a specific test or to the run as a whole. For details, see [Message bus file artifacts](./microsoft-testing-platform-architecture-extensions.md#message-bus-file-artifacts). + +- Run-level or session-level files are standalone bus messages: publish a `FileArtifact` (unscoped) or a `SessionFileArtifact` (scoped to the run through its `SessionUid`). +- Test-specific attachments aren't standalone messages. Add them as `FileArtifactProperty` entries on `TestNode.Properties` inside a `TestNodeUpdateMessage`, so the terminal, `dotnet test`, and IDEs associate each file with its test. + +If you intend to publish a `SessionFileArtifact`, you must declare that runtime type to the platform in advance through `DataTypesProduced`, as shown below. Note that `FileArtifactProperty` isn't listed separately, because it travels inside a `TestNodeUpdateMessage` rather than as its own message type: ```csharp internal sealed class TestingFramework diff --git a/docs/core/testing/microsoft-testing-platform-terminal-output.md b/docs/core/testing/microsoft-testing-platform-terminal-output.md index fac528fb6028a..b8bc695f27000 100644 --- a/docs/core/testing/microsoft-testing-platform-terminal-output.md +++ b/docs/core/testing/microsoft-testing-platform-terminal-output.md @@ -3,7 +3,7 @@ title: Microsoft.Testing.Platform (MTP) terminal output description: Learn about the built-in terminal test reporter in MTP, including output modes, ANSI support, and progress indicators. author: evangelink ms.author: amauryleve -ms.date: 06/16/2026 +ms.date: 07/17/2026 ai-usage: ai-assisted --- @@ -41,6 +41,14 @@ The progress bar is written based on the selected mode: - ANSI, the progress bar is animated, sticking to the bottom of the screen and is refreshed every 500ms. The progress bar hides once test execution is done. - non-ANSI, the progress bar is written to screen as is every 3 seconds. The progress remains in the output. +### Direct console output and progress redraw + +To animate the progress bar, the ANSI progress renderer takes control of the terminal cursor and repeatedly redraws the bottom of the screen. Any text that's written directly to `stdout` or `stderr` outside the test framework's capture path can be overwritten or removed during this redraw. For example, a `Console.WriteLine` call from assembly-level or session-level lifecycle code (such as a `Before(Assembly)` or `Before(TestSession)` hook), or from an extension, might flash briefly and then disappear when the progress bar refreshes. + +This behavior is distinct from *captured* per-test standard output and standard error. Output that a test writes while it runs is captured by the framework and shown according to `--show-stdout` and `--show-stderr`, so the progress bar doesn't overwrite it. + +If your code must write directly to the console and you need that output to remain visible, disable progress (`--progress off` in MTP 2.3.0+, or `--no-progress` in earlier versions). Alternatively, disable ANSI (`--ansi off` in MTP 2.3.0+, or `--no-ansi` in earlier versions) to use the non-ANSI progress output, which appends new lines instead of redrawing in place and doesn't overwrite prior direct output. + ## Options | Option | MTP version | Description | diff --git a/docs/csharp/advanced-topics/update-applications/nullable-migration-strategies.md b/docs/csharp/advanced-topics/update-applications/nullable-migration-strategies.md index f9acdb9040135..6f5c2a08f7744 100644 --- a/docs/csharp/advanced-topics/update-applications/nullable-migration-strategies.md +++ b/docs/csharp/advanced-topics/update-applications/nullable-migration-strategies.md @@ -3,7 +3,7 @@ title: Nullable migration strategies description: Learn how to enable nullable reference types in an existing C# codebase. Choose a default context, address warnings progressively, and fully migrate. ms.date: 05/04/2026 ms.topic: concept-article -ms.subservice: null-safety +ms.subservice: nullable-reference-types ai-usage: ai-assisted --- # Nullable migration strategies diff --git a/docs/csharp/fundamentals/null-safety/common-tasks/resolve-warnings.md b/docs/csharp/fundamentals/null-safety/common-tasks/resolve-warnings.md index 5a04e5370307d..f62d5d4f97483 100644 --- a/docs/csharp/fundamentals/null-safety/common-tasks/resolve-warnings.md +++ b/docs/csharp/fundamentals/null-safety/common-tasks/resolve-warnings.md @@ -3,7 +3,7 @@ title: Resolve nullable warnings description: Learn the five techniques you use to address nullable warnings in C# and make your code resilient to null reference exceptions. ms.date: 05/04/2026 ms.topic: concept-article -ms.subservice: null-safety +ms.subservice: nullable-reference-types ai-usage: ai-assisted --- # Resolve nullable warnings diff --git a/docs/csharp/fundamentals/null-safety/nullable-reference-types.md b/docs/csharp/fundamentals/null-safety/nullable-reference-types.md index 04473b0c63e23..550aedb00de29 100644 --- a/docs/csharp/fundamentals/null-safety/nullable-reference-types.md +++ b/docs/csharp/fundamentals/null-safety/nullable-reference-types.md @@ -3,7 +3,7 @@ title: Nullable reference types description: Use nullable reference types to express your intent about which variables can be null. The compiler then warns when your code might dereference null. ms.date: 05/04/2026 ms.topic: concept-article -ms.subservice: null-safety +ms.subservice: nullable-reference-types ai-usage: ai-assisted --- # Nullable reference types diff --git a/docs/csharp/fundamentals/statements/collections.md b/docs/csharp/fundamentals/statements/collections.md new file mode 100644 index 0000000000000..8c7ab426451f6 --- /dev/null +++ b/docs/csharp/fundamentals/statements/collections.md @@ -0,0 +1,97 @@ +--- +title: "Common collection types in C#" +description: Store, update, search, and read groups of values by using arrays, lists, dictionaries, collection expressions, indexes, and ranges. +ms.date: 07/15/2026 +ms.topic: concept-article +ai-usage: ai-assisted +--- + +# Common collection types + +> [!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. For more information about collections, see [Collections](../../language-reference/builtin-types/collections.md) in the language reference. For more information about arrays, see [The array reference type](../../language-reference/builtin-types/arrays.md) in the language reference. For more information about collection expressions, see [Collection expressions (Collection literals)](../../language-reference/operators/collection-expressions.md) in the language reference. +> +> **Coming from another language?** C# arrays are fixed-size ordered collections, like arrays in Java or C++. is the everyday growable sequence, like Java's `ArrayList`, JavaScript's arrays, or Python's lists. stores values by key, like maps or dictionaries in many languages. + +A *collection* is an object that stores multiple related values. Each value in a collection is an *element*. Choose an array when the set is fixed, such as the status names your work-item tracker always uses. Choose a when items come and go, such as a backlog that gains new work and drops completed items. Choose a when you look up values by a key, such as finding a work item by ID or a setting by name. The next section turns those examples into a quick decision model. + +## Choose a collection shape + +Choose the collection that matches how your code uses the data. A *sequence* stores elements in order so you can reach them by position. Use an *array*, which is represented by , when you know the number of positions the sequence needs. An array's length can't change after you create it, but you can replace the element stored at an existing position. Use when you need a sequence that can add or remove elements. A *dictionary* stores values that you reach by key instead of by position. Use when each element has a lookup key, such as a name, ID, or code. The examples use a *collection expression*, which creates a collection from expressions between square brackets. The [Create collections with collection expressions](#create-collections-with-collection-expressions) section explains that syntax in more detail. + +:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="ChooseCollection"::: + + and are *generic types*. A generic type uses a type argument, such as `string` or `int`, to say what kind of values it stores. Generics let you reuse the same collection shape, such as a list or dictionary, with different element types, such as `string`, `int`, or a custom type. They also provide *type safety*: the compiler guarantees every element is the declared type, so you don't need casts and can't accidentally store the wrong type. For more information about generic types, see [Generic types and methods](../types/generics.md) in the fundamentals. + +## Store fixed-size data in arrays + +An array is an ordered collection with a fixed length. You access an array element by its *index*, which is its zero-based position in the array. Index `0` is the first element, index `1` is the second element, and so on. + +:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="Arrays"::: + +Use `foreach` when you want to read every element in order. Use an index when the position matters, such as when you need the first element, the last element, or the position returned by . + +The length of an array is fixed, but the elements in that array can change. Assign a new value to an existing index when the position stays the same but the stored value needs an update: + +:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="ArrayElementUpdate"::: + +## Grow and shrink a sequence with `List` + +A stores elements in order and can grow or shrink as your program runs. Use to append an element, to remove a matching element, to test whether an element exists, and to find an element's position. Use the list indexer to replace the value at an existing position. + +:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="ListChanges"::: + +A keeps the remaining elements in order when you add or remove items. When you remove an element, later elements move to lower indexes. + +Use to add one element at a specific position, to add several elements at a position, and to remove an element by index: + +:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="ListInsertRemove"::: + +Adding or removing at the end of a is fast. Adding with is an O(1) operation on average, and removing the last element with is O(1). Inserting or removing at the front or middle is O(n) because every later element shifts to a new index. If your code frequently inserts or removes at the front, a might be the wrong collection shape. + +> [!TIP] +> Big-O notation describes how the work grows as the collection size, `n`, grows. O(1), pronounced "order one," means the operation takes about the same amount of work no matter how many elements the collection contains. O(n), pronounced "order n," means the work grows roughly in proportion to the number of elements in the collection. + +## Associate a value by key with `Dictionary` + +A stores key/value pairs. A *key/value pair* is one key and the value associated with that key; .NET represents one pair with . Use the dictionary indexer to add or update a value by key, and use when the key might not exist. + +:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="DictionaryLookup"::: + +An *indexer* lets you use bracket syntax to access a value from an object. The dictionary indexer is useful when the key must exist or when you're assigning a value. checks whether a key exists and gives you the value when it does. For more information about the indexer operator, see [Member access operators](../../language-reference/operators/member-access-operators.md#indexer-operator-) in the language reference. + +You can also change the value associated with a key that already exists. Assign through the indexer to add a key or replace the value for an existing key. If the replacement depends on the current value, read the value first, then assign the new value: + +:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="DictionaryUpdates"::: + +## Create collections with collection expressions + +A *collection expression* creates a collection from expressions between square brackets. Beginning with C# 12, collection expressions can create arrays, lists, and other collection types. A *spread element* copies the elements from another collection into the new collection. + +:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="CollectionExpressions"::: + +Collection expressions keep initialization concise. A collection expression has no type of its own. The compiler converts the expression into the collection shape the context calls for. The receiving variable or parameter can turn it into an array, a , or other supported collection type. + +## Read from positions with indexes and ranges + +Indexes answer "which single element?" Ranges answer "which contiguous slice?" Use indexes when your code needs one element and ranges when the subsection is part of the data you're working with. Arrays and `List` support both indexes and ranges. + +An can count from the start or from the end of a sequence. Use the number directly to count from the start: `phases[0]` reads the first element, and `phases[1]` reads the second element. Use `^` to count from the end: `phases[^1]` reads the last element, and `phases[^2]` reads the next-to-last element. + +An selects a slice with `..` (two dots). The range operator isn't the same as the spread element you saw earlier in collection expressions, even though both use dots. Use a range `a..b` when you want elements from index `a` up to, but ***not including***, index `b`. The expression `phases[1..3]` creates a new array that contains the elements at index `1` and index `2`. + +You can omit one or both range indexes. Omit the start index when the slice starts at the beginning, as in `phases[..2]` for the first two elements. Omit the end index when the slice continues through the last element, as in `phases[2..]` for the elements from index `2` to the end. Omit both indexes when you want a copy of the whole array, as in `phases[..]`. A range can mix index kinds. The expression `phases[1..^1]` combines a from-start index and a from-end index to return the middle elements, `code` and `test`. + +:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="IndexesAndRanges"::: + +For more information about indexes and ranges, see [Explore ranges of data using indices and ranges](../../tutorials/ranges-indexes.md) in the tutorials. + +## See also + +- [Iteration statements](iteration.md) +- [LINQ queries](linq.md) +- [Collections](../../language-reference/builtin-types/collections.md) +- [Arrays](../../language-reference/builtin-types/arrays.md) +- [Collection expressions](../../language-reference/operators/collection-expressions.md) +- [Member access operators](../../language-reference/operators/member-access-operators.md) +- [Explore indexes and ranges](../../tutorials/ranges-indexes.md) diff --git a/docs/csharp/fundamentals/statements/linq.md b/docs/csharp/fundamentals/statements/linq.md new file mode 100644 index 0000000000000..bb62509640a51 --- /dev/null +++ b/docs/csharp/fundamentals/statements/linq.md @@ -0,0 +1,120 @@ +--- +title: "LINQ queries in C#" +description: Query collections with LINQ by using query syntax, method syntax, common operators, and lambda expressions. +ms.date: 07/15/2026 +ms.topic: concept-article +ai-usage: ai-assisted +--- + +# LINQ queries + +> [!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. For more information about providers, operators, and advanced scenarios, see [Language Integrated Query (LINQ)](../../linq/index.md). +> +> **Coming from another language?** LINQ query syntax reads like a SQL-style query written inside C#. LINQ method syntax reads like chained collection operations in JavaScript, Java streams, or Python pipelines. Both forms describe the same query. + +*Language Integrated Query (LINQ)* is the C# feature set for querying data with C# syntax. Many LINQ operators use *deferred execution*: they describe the result first and read the data later, when your code asks for the results. A *query* describes which data to read and how to shape the result. A query reads from a *data source*. A data source can be an in-memory collection, such as an array or , or an external source, such as a database or XML, exposed through a LINQ provider. A *LINQ provider* is a library that connects LINQ syntax to a specific kind of data source. This article uses in-memory collections for its examples. A *sequence* is an ordered set of elements represented by . For more information about provider-based queries, see [Language Integrated Query (LINQ)](../../linq/index.md). + +## LINQ query expression syntax + +The examples in this article read in-memory collections such as arrays and . A query usually has three parts: specify the data source, describe the result, and enumerate the source to produce the result. To *enumerate* a sequence means to read its elements one at a time, often with `foreach`. + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="QuerySyntax"::: + +The query expression starts with `from` to name the data source and range variable. The `where` clause keeps only matching elements, `orderby` sorts them, and `select` shapes the result. + +## LINQ method syntax + +*Query syntax* uses clauses such as `from`, `where`, `orderby`, and `select`. *Method syntax* calls LINQ methods directly. For in-memory sequences, the standard LINQ methods are the built-in methods for filtering, projection, sorting, grouping, and related operations. + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="MethodSyntax"::: + +Method syntax is also called *fluent syntax* because each call returns a result that the next call can use. Many queries can use either form. Use the form that makes the query easiest to read. + +Query syntax often reads well when the query has several clauses. The `let` clause gives a name to an intermediate value before the query filters, sorts, and selects the final result: + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="QuerySyntaxClearer"::: + +Method syntax often reads well for short operations. It's necessary for methods that don't have query-syntax keyword. For example, returns one value directly: + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="MethodSyntaxClearer"::: + +## Lambda expressions and LINQ + +A *lambda expression* is an anonymous function that you can pass as an argument. LINQ method syntax commonly uses lambda expressions to say what each operator should do with each element. + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="LambdaExpressions"::: + +In `name => name.Length == 3`, `name` is the input element and `name.Length == 3` is the Boolean expression that decides whether the element belongs in the result. For more information about lambda expressions and delegate types, see [Lambda expressions, delegates, and events](../types/delegates-lambdas.md) in the fundamentals. + +Query-syntax clauses use lambda expressions too. Clauses such as `where`, `orderby`, and `select` compile to method calls that take lambda expressions. The range variable becomes the lambda parameter, and the clause expression becomes the lambda body. Query syntax is a concise way to write those same lambdas: + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="QuerySyntaxLambda"::: + +## Common LINQ methods + +The most common LINQ methods cover the everyday steps of a query: filter data, shape results, sort, group, and summarize. These methods are in the namespace and work with sequences such as . + +- keeps only the elements that match a condition. +- transforms each element into a new value. C# calls this operation a *projection*. +- sorts elements. +- creates groups of elements that share a key. +- Aggregation methods such as , , and produce a single value from all elements in the data source. + +> [!NOTE] +> If you know the traditional functional-programming terms, is a *filter*, is a *map* (C# calls it a projection), and aggregation methods such as , , and are a *reduce*. + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="CommonOperators"::: + +A *projection* creates a result value from each input element. In the previous example, the projection creates strings that combine a work item name and its priority. + +### Group related values + +Use when the result should contain groups of elements that share a key. Each group is represented by , which exposes the group key and the elements in that group. + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="GroupBy"::: + +Grouping is useful for summaries, reports, and menus. For more information about joins, nested groupings, and provider-specific behavior, see [Language Integrated Query (LINQ)](../../linq/index.md). + +## Run a query + +Many LINQ operators use *deferred execution*. Deferred execution means operators that return a sequence, such as , , and , don't run when you define them. They build the recipe for producing results. A `foreach` loop is one way to run that recipe: + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="DeferredExecution"::: + +Other operations run a query immediately. Operators that return a single value, such as , , , and , must read the elements when you call them so they can produce that value. + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="ImmediateExecution"::: + +There's no single trigger that runs a query. *Eager evaluation*, also called immediate evaluation, runs the query right away and stores or returns the result. Scalar and aggregate operators such as , , , and use eager evaluation. So do and when you need a snapshot of the current results. For more information about deferred execution, see [Introduction to LINQ Queries](../../linq/get-started/introduction-to-linq-queries.md) in the LINQ documentation. + +## Compose queries + +Real queries often grow from smaller decisions: start with a shared filter, add a sort for one screen, or apply an optional condition from user input. By composing queries, you can keep each step readable and reuse common parts instead of repeating one large query. + +Imagine an app that shows open work items in several places. One view needs all open items, and another view needs only the highest-priority open items. To *compose* a query, start with a base query stored in a variable, then build a more specific query from it. Because execution is deferred, each step describes more of the result. No work happens until you enumerate the final query. + +The following example stores the open work items in one query, then reuses that query to find the highest-priority open items: + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="ComposeQuerySyntax"::: + +You can also compose queries with method syntax. The next example starts with all open items, then conditionally adds another filter before it selects the titles: + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="ComposeMethodSyntax"::: + +Composition can use either or both *eager evaluation* and *deferred execution*. Materialize a shared intermediate result with or when you want that part to run once and stay fixed. Then build another deferred query from the cached results for the final output: + +:::code language="csharp" source="./snippets/linq-statements/Program.cs" id="ComposeWithCaching"::: + +## Explore LINQ in depth + +This article uses in-memory collections to teach LINQ syntax and core operators. For more information about LINQ operators and advanced scenarios, see [Language Integrated Query (LINQ)](../../linq/index.md). That article covers joins, XML, database providers, dynamic queries, and custom operators. + +## See also + +- [Collections](collections.md) +- [Introduction to LINQ queries](../../linq/get-started/introduction-to-linq-queries.md) +- [Write LINQ queries](../../linq/get-started/write-linq-queries.md) +- [Standard query operators](../../linq/standard-query-operators/index.md) +- [Lambda expressions](../../language-reference/operators/lambda-expressions.md) +- [LINQ and collections](../../linq/how-to-query-collections.md) diff --git a/docs/csharp/fundamentals/statements/snippets/collections-statements/Program.cs b/docs/csharp/fundamentals/statements/snippets/collections-statements/Program.cs new file mode 100644 index 0000000000000..2b304f5fd33a0 --- /dev/null +++ b/docs/csharp/fundamentals/statements/snippets/collections-statements/Program.cs @@ -0,0 +1,247 @@ +namespace CollectionStatements; + +public static class Program +{ + public static void Main() + { + ChooseCollectionExample(); + ArraysExample(); + ArrayElementUpdateExample(); + ListChangesExample(); + ListInsertRemoveExample(); + DictionaryLookupExample(); + DictionaryUpdatesExample(); + CollectionExpressionsExample(); + IndexesAndRangesExample(); + } + + private static void ChooseCollectionExample() + { + // + string[] sprintPlan = ["design", "code", "test"]; + List backlog = ["design", "code"]; + Dictionary priorities = new() + { + ["docs"] = 2, + ["tests"] = 1 + }; + + backlog.Add("test"); + + Console.WriteLine($"Array: {string.Join(", ", sprintPlan)}"); + Console.WriteLine($"List count: {backlog.Count}"); + Console.WriteLine($"Priority for docs: {priorities["docs"]}"); + // This example produces the following output: + // + // Array: design, code, test + // List count: 3 + // Priority for docs: 2 + // + // + } + + private static void ArraysExample() + { + // + string[] stages = ["design", "code", "test", "review"]; + + Console.WriteLine($"First: {stages[0]}"); + Console.WriteLine($"test index: {Array.IndexOf(stages, "test")}"); + // This example produces the following output: + // + // First: design + // test index: 2 + // + // + } + + private static void ArrayElementUpdateExample() + { + // + string[] stages = ["design", "code", "test"]; + + stages[0] = "plan"; + + Console.WriteLine(string.Join(", ", stages)); + Console.WriteLine($"Length: {stages.Length}"); + // This example produces the following output: + // + // plan, code, test + // Length: 3 + // + // + } + + private static void ListChangesExample() + { + // + List workItems = ["design", "code", "test"]; + + workItems.Add("review"); + workItems.Remove("code"); + workItems[1] = "verify"; + + Console.WriteLine(string.Join(", ", workItems)); + Console.WriteLine($"Has review: {workItems.Contains("review")}"); + Console.WriteLine($"Index of verify: {workItems.IndexOf("verify")}"); + // This example produces the following output: + // + // design, verify, review + // Has review: True + // Index of verify: 1 + // + // + } + + private static void ListInsertRemoveExample() + { + // + List workItems = ["design", "test"]; + + workItems.Insert(1, "code"); + Console.WriteLine($"Insert middle: {string.Join(", ", workItems)}"); + + workItems.Insert(0, "plan"); + Console.WriteLine($"Insert front: {string.Join(", ", workItems)}"); + + workItems.InsertRange(workItems.Count, ["review", "deploy"]); + Console.WriteLine($"Insert range at end: {string.Join(", ", workItems)}"); + + workItems.RemoveAt(workItems.Count - 1); + Console.WriteLine($"Remove end: {string.Join(", ", workItems)}"); + + workItems.RemoveAt(2); + Console.WriteLine($"Remove middle: {string.Join(", ", workItems)}"); + + workItems.RemoveAt(0); + Console.WriteLine($"Remove front: {string.Join(", ", workItems)}"); + // This example produces the following output: + // + // Insert middle: design, code, test + // Insert front: plan, design, code, test + // Insert range at end: plan, design, code, test, review, deploy + // Remove end: plan, design, code, test, review + // Remove middle: plan, design, test, review + // Remove front: design, test, review + // + // + } + + private static void DictionaryLookupExample() + { + // + Dictionary priorities = new() + { + ["docs"] = 2, + ["tests"] = 1 + }; + + priorities["review"] = 3; + priorities.Remove("review"); + + if (priorities.TryGetValue("docs", out int docsPriority)) + { + Console.WriteLine($"docs priority: {docsPriority}"); + } + else + { + Console.WriteLine("docs missing"); + } + + if (priorities.TryGetValue("deploy", out int deployPriority)) + { + Console.WriteLine($"deploy priority: {deployPriority}"); + } + else + { + Console.WriteLine("deploy missing"); + } + + Console.WriteLine($"count: {priorities.Count}"); + // This example produces the following output: + // + // docs priority: 2 + // deploy missing + // count: 2 + // + // + } + + private static void DictionaryUpdatesExample() + { + // + Dictionary priorities = new() + { + ["docs"] = 2, + ["tests"] = 1 + }; + + priorities["docs"] = 1; + + if (priorities.TryGetValue("tests", out int testsPriority)) + { + priorities["tests"] = testsPriority + 1; + } + + priorities["deploy"] = 3; + + Console.WriteLine($"docs priority: {priorities["docs"]}"); + Console.WriteLine($"tests priority: {priorities["tests"]}"); + Console.WriteLine($"deploy priority: {priorities["deploy"]}"); + // This example produces the following output: + // + // docs priority: 1 + // tests priority: 2 + // deploy priority: 3 + // + // + } + + private static void CollectionExpressionsExample() + { + // + string[] planned = ["design", "code"]; + // The spread element .. planned copies each element from planned. + string[] upcoming = [.. planned, "test"]; + List blocked = ["docs"]; + + Console.WriteLine($"Upcoming: {string.Join(", ", upcoming)}"); + Console.WriteLine($"Blocked: {string.Join(", ", blocked)}"); + // This example produces the following output: + // + // Upcoming: design, code, test + // Blocked: docs + // + // + } + + private static void IndexesAndRangesExample() + { + // + string[] phases = ["design", "code", "test", "deploy"]; + List checklist = ["design", "test", "review"]; + + Console.WriteLine($"Last: {phases[^1]}"); + Console.WriteLine($"Middle: {string.Join(", ", phases[1..3])}"); + Console.WriteLine($"Last two: {string.Join(", ", phases[^2..])}"); + Console.WriteLine($"Without ends: {string.Join(", ", phases[1..^1])}"); + Console.WriteLine($"First two: {string.Join(", ", phases[..2])}"); + Console.WriteLine($"From third: {string.Join(", ", phases[2..])}"); + Console.WriteLine($"All phases: {string.Join(", ", phases[..])}"); + Console.WriteLine($"List last: {checklist[^1]}"); + Console.WriteLine($"List first two: {string.Join(", ", checklist[0..2])}"); + // This example produces the following output: + // + // Last: deploy + // Middle: code, test + // Last two: test, deploy + // Without ends: code, test + // First two: design, code + // From third: test, deploy + // All phases: design, code, test, deploy + // List last: review + // List first two: design, test + // + // + } +} diff --git a/docs/csharp/fundamentals/statements/snippets/collections-statements/collections-statements.csproj b/docs/csharp/fundamentals/statements/snippets/collections-statements/collections-statements.csproj new file mode 100644 index 0000000000000..843453f90596c --- /dev/null +++ b/docs/csharp/fundamentals/statements/snippets/collections-statements/collections-statements.csproj @@ -0,0 +1,9 @@ + + + Exe + net10.0 + enable + enable + + + diff --git a/docs/csharp/fundamentals/statements/snippets/linq-statements/Program.cs b/docs/csharp/fundamentals/statements/snippets/linq-statements/Program.cs new file mode 100644 index 0000000000000..440ba6147a9d1 --- /dev/null +++ b/docs/csharp/fundamentals/statements/snippets/linq-statements/Program.cs @@ -0,0 +1,345 @@ +namespace LinqStatements; + +public static class Program +{ + public static void Main() + { + QuerySyntaxExample(); + MethodSyntaxExample(); + QuerySyntaxClearerExample(); + MethodSyntaxClearerExample(); + LambdaExpressionsExample(); + QuerySyntaxLambdaExample(); + CommonOperatorsExample(); + GroupByExample(); + DeferredExecutionExample(); + ImmediateExecutionExample(); + ComposeQuerySyntaxExample(); + ComposeMethodSyntaxExample(); + ComposeWithCachingExample(); + } + + private static void QuerySyntaxExample() + { + // + string[] names = ["Ana", "Ben", "Cleo", "Dara"]; + + IEnumerable query = + from name in names + where name.Length >= 4 + orderby name + select name; + + foreach (string name in query) + { + Console.WriteLine(name); + } + // This example produces the following output: + // + // Cleo + // Dara + // + // + } + + private static void MethodSyntaxExample() + { + // + string[] names = ["Ana", "Ben", "Cleo", "Dara"]; + + IEnumerable query = names + .Where(name => name.Length >= 4) + .OrderBy(name => name) + .Select(name => name); + + foreach (string name in query) + { + Console.WriteLine(name); + } + // This example produces the following output: + // + // Cleo + // Dara + // + // + } + + private static void QuerySyntaxClearerExample() + { + // + (string Area, int Priority)[] workItems = + [ + ("docs", 2), + ("tests", 1), + ("deploy", 4), + ("api", 1) + ]; + + IEnumerable nextItems = + from item in workItems + let label = $"{item.Area}: P{item.Priority}" + where item.Priority <= 2 + orderby item.Priority, item.Area + select label; + + foreach (string item in nextItems) + { + Console.WriteLine(item); + } + // This example produces the following output: + // + // api: P1 + // tests: P1 + // docs: P2 + // + // + } + + private static void MethodSyntaxClearerExample() + { + // + List workItems = ["design", "docs", "deploy", "review"]; + + int count = workItems.Count(item => item.StartsWith('d')); + + Console.WriteLine($"Starts with d: {count}"); // => Starts with d: 3 + // + } + + private static void LambdaExpressionsExample() + { + // + string[] names = ["Ana", "Ben", "Cleo"]; + + IEnumerable shortNames = names + .Where(name => name.Length == 3) + .Select(name => name.ToUpperInvariant()); + + foreach (string name in shortNames) + { + Console.WriteLine(name); + } + // This example produces the following output: + // + // ANA + // BEN + // + // + } + + private static void QuerySyntaxLambdaExample() + { + // + string[] workItems = ["docs", "test", "deploy"]; + + IEnumerable querySyntax = + from item in workItems + where item.Length == 4 + select item; + + IEnumerable methodSyntax = + workItems.Where(item => item.Length == 4); + + foreach (string item in querySyntax) + { + Console.WriteLine($"Result: {item}"); + } + + foreach (string item in methodSyntax) + { + Console.WriteLine($"Result: {item}"); + } + // This example produces the following output: + // + // Result: docs + // Result: test + // Result: docs + // Result: test + // + // + } + + private static void CommonOperatorsExample() + { + // + Dictionary priorities = new() + { + ["Docs"] = 2, + ["Code"] = 1, + ["Test"] = 3, + ["Deploy"] = 4 + }; + + IEnumerable plannedWork = priorities + .Where(workItem => workItem.Value <= 3) + .OrderBy(workItem => workItem.Value) + .Select(workItem => $"{workItem.Key}: {workItem.Value}"); + + foreach (string item in plannedWork) + { + Console.WriteLine(item); + } + // This example produces the following output: + // + // Code: 1 + // Docs: 2 + // Test: 3 + // + // + } + + private static void GroupByExample() + { + // + string[] labels = ["api", "auth", "docs", "deploy"]; + + IEnumerable> groups = labels.GroupBy(label => label[0]); + + foreach (IGrouping group in groups) + { + Console.WriteLine($"{group.Key}: {string.Join(", ", group)}"); + } + // This example produces the following output: + // + // a: api, auth + // d: docs, deploy + // + // + } + + private static void DeferredExecutionExample() + { + // + List workItems = ["design", "docs"]; + + IEnumerable query = workItems.Where(item => item.StartsWith('d')); + + workItems.Add("deploy"); + + // The query runs here, when foreach asks for the results. + foreach (string item in query) + { + Console.WriteLine(item); + } + // This example produces the following output: + // + // design + // docs + // deploy + // + // + } + + private static void ImmediateExecutionExample() + { + // + List workItems = ["design", "docs"]; + + IEnumerable query = workItems.Where(item => item.StartsWith('d')); + + // Count() reads the matching elements right when this line runs. + int count = query.Count(); + Console.WriteLine($"Count before add: {count}"); + + workItems.Add("deploy"); + + Console.WriteLine($"Stored count: {count}"); + Console.WriteLine($"Current count: {query.Count()}"); + // This example produces the following output: + // + // Count before add: 2 + // Stored count: 2 + // Current count: 3 + // + // + } + + private static void ComposeQuerySyntaxExample() + { + // + (string Title, int Priority, bool IsOpen)[] items = + [ + ("docs", 2, true), + ("tests", 1, true), + ("deploy", 3, false), + ("api", 1, true) + ]; + + IEnumerable<(string Title, int Priority, bool IsOpen)> openItems = + from item in items + where item.IsOpen + select item; + + IEnumerable topOpenItems = + from item in openItems + where item.Priority == 1 + orderby item.Title + select item.Title; + + foreach (string title in topOpenItems) + { + Console.WriteLine(title); + } + // This example produces the following output: + // + // api + // tests + // + // + } + + private static void ComposeMethodSyntaxExample() + { + // + (string Title, string Area, bool IsOpen)[] items = + [ + ("write docs", "docs", true), + ("fix tests", "tests", true), + ("deploy site", "deploy", false) + ]; + + bool onlyDocs = true; + + IEnumerable<(string Title, string Area, bool IsOpen)> query = + items.Where(item => item.IsOpen); + + if (onlyDocs) + { + query = query.Where(item => item.Area == "docs"); + } + + foreach (string title in query.Select(item => item.Title)) + { + Console.WriteLine(title); // => write docs + } + // + } + + private static void ComposeWithCachingExample() + { + // + List<(string Title, int Priority, bool IsOpen)> items = + [ + ("docs", 2, true), + ("tests", 1, true), + ("deploy", 3, false) + ]; + + List<(string Title, int Priority, bool IsOpen)> openItems = items + .Where(item => item.IsOpen) + .ToList(); + + items.Add(("api", 1, true)); + + IEnumerable cachedTopOpenItems = openItems + .Where(item => item.Priority == 1) + .OrderBy(item => item.Title) + .Select(item => item.Title); + + foreach (string title in cachedTopOpenItems) + { + Console.WriteLine(title); // => tests + } + // + } +} diff --git a/docs/csharp/fundamentals/statements/snippets/linq-statements/linq-statements.csproj b/docs/csharp/fundamentals/statements/snippets/linq-statements/linq-statements.csproj new file mode 100644 index 0000000000000..843453f90596c --- /dev/null +++ b/docs/csharp/fundamentals/statements/snippets/linq-statements/linq-statements.csproj @@ -0,0 +1,9 @@ + + + Exe + net10.0 + enable + enable + + + diff --git a/docs/csharp/fundamentals/tutorials/nullable-reference-types.md b/docs/csharp/fundamentals/tutorials/nullable-reference-types.md index 6b2cd16aed837..0f0e7cc57a949 100644 --- a/docs/csharp/fundamentals/tutorials/nullable-reference-types.md +++ b/docs/csharp/fundamentals/tutorials/nullable-reference-types.md @@ -3,7 +3,7 @@ title: "Tutorial: Express your design intent with nullable and non-nullable refe description: Build a small survey app that uses nullable and non-nullable reference types to declare which references can be null and have the compiler enforce that intent. ms.date: 05/04/2026 ms.topic: tutorial -ms.subservice: null-safety +ms.subservice: nullable-reference-types ai-usage: ai-assisted #customer intent: As a C# developer, I want to use nullable and non-nullable reference types in a real design so that the compiler catches null-handling mistakes for me. diff --git a/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst.CodeFixes/MakeConst.CodeFixes.csproj b/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst.CodeFixes/MakeConst.CodeFixes.csproj index 991d144b3db82..34bebb27c13f2 100644 --- a/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst.CodeFixes/MakeConst.CodeFixes.csproj +++ b/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst.CodeFixes/MakeConst.CodeFixes.csproj @@ -7,6 +7,7 @@ + diff --git a/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst.Test/MakeConst.Test.csproj b/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst.Test/MakeConst.Test.csproj index 9f5bd48993901..a6a2babaddc13 100644 --- a/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst.Test/MakeConst.Test.csproj +++ b/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst.Test/MakeConst.Test.csproj @@ -11,7 +11,7 @@ - + diff --git a/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst/MakeConst.csproj b/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst/MakeConst.csproj index 2efb7926c60ba..12bc57d45d344 100644 --- a/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst/MakeConst.csproj +++ b/docs/csharp/roslyn-sdk/tutorials/snippets/how-to-write-csharp-analyzer-code-fix/MakeConst/MakeConst/MakeConst.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index 4c2d53605975b..511cb5c2df4a0 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -119,6 +119,10 @@ items: href: fundamentals/statements/selection.md - name: Iteration statements href: fundamentals/statements/iteration.md + - name: Collections + href: fundamentals/statements/collections.md + - name: LINQ + href: fundamentals/statements/linq.md - name: Object-oriented programming items: - name: Classes, structs, and records diff --git a/docs/fundamentals/toc.yml b/docs/fundamentals/toc.yml index 57efc41124d8b..fe707ee890ac6 100644 --- a/docs/fundamentals/toc.yml +++ b/docs/fundamentals/toc.yml @@ -411,7 +411,7 @@ items: href: ../standard/memory-and-spans/index.md - name: Memory and Span usage guidelines href: ../standard/memory-and-spans/memory-t-usage-guidelines.md - - name: SIMD-enabled types + - name: SIMD and hardware intrinsics href: ../standard/simd.md - name: Value tuples href: ../standard/value-tuples.md diff --git a/docs/orleans/Directory.Build.props b/docs/orleans/Directory.Build.props index 367bc46c74f70..56379750a2e76 100644 --- a/docs/orleans/Directory.Build.props +++ b/docs/orleans/Directory.Build.props @@ -15,7 +15,7 @@ - + diff --git a/docs/orleans/host/snippets/aspire/Client/Client.csproj b/docs/orleans/host/snippets/aspire/Client/Client.csproj index 2a015225101e7..3ce46488bfc8e 100644 --- a/docs/orleans/host/snippets/aspire/Client/Client.csproj +++ b/docs/orleans/host/snippets/aspire/Client/Client.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/docs/orleans/host/snippets/aspire/ServiceDefaults/ServiceDefaults.csproj b/docs/orleans/host/snippets/aspire/ServiceDefaults/ServiceDefaults.csproj index 1b896e9d451b7..7243f55396099 100644 --- a/docs/orleans/host/snippets/aspire/ServiceDefaults/ServiceDefaults.csproj +++ b/docs/orleans/host/snippets/aspire/ServiceDefaults/ServiceDefaults.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/docs/orleans/host/snippets/aspire/Silo/Silo.csproj b/docs/orleans/host/snippets/aspire/Silo/Silo.csproj index c9166c17c8efb..b9ad04a2dfefe 100644 --- a/docs/orleans/host/snippets/aspire/Silo/Silo.csproj +++ b/docs/orleans/host/snippets/aspire/Silo/Silo.csproj @@ -6,15 +6,15 @@ - - + + - + diff --git a/docs/orleans/tutorials-and-samples/snippets/minimal/Client/Client.csproj b/docs/orleans/tutorials-and-samples/snippets/minimal/Client/Client.csproj index a2f56df87a8a4..4d386f0fb4a9f 100644 --- a/docs/orleans/tutorials-and-samples/snippets/minimal/Client/Client.csproj +++ b/docs/orleans/tutorials-and-samples/snippets/minimal/Client/Client.csproj @@ -5,9 +5,9 @@ - + - + diff --git a/docs/orleans/tutorials-and-samples/snippets/minimal/GrainInterfaces/GrainInterfaces.csproj b/docs/orleans/tutorials-and-samples/snippets/minimal/GrainInterfaces/GrainInterfaces.csproj index fafc4bf222ed2..d0ae79ded1357 100644 --- a/docs/orleans/tutorials-and-samples/snippets/minimal/GrainInterfaces/GrainInterfaces.csproj +++ b/docs/orleans/tutorials-and-samples/snippets/minimal/GrainInterfaces/GrainInterfaces.csproj @@ -1,10 +1,10 @@ - + - + diff --git a/docs/orleans/tutorials-and-samples/snippets/minimal/Grains/Grains.csproj b/docs/orleans/tutorials-and-samples/snippets/minimal/Grains/Grains.csproj index 6e90bd80377ae..883fbe6f57f14 100644 --- a/docs/orleans/tutorials-and-samples/snippets/minimal/Grains/Grains.csproj +++ b/docs/orleans/tutorials-and-samples/snippets/minimal/Grains/Grains.csproj @@ -1,7 +1,7 @@ - + diff --git a/docs/orleans/tutorials-and-samples/snippets/minimal/Silo/Silo.csproj b/docs/orleans/tutorials-and-samples/snippets/minimal/Silo/Silo.csproj index e4fb5a6462fb0..b8f370b1c9b6f 100644 --- a/docs/orleans/tutorials-and-samples/snippets/minimal/Silo/Silo.csproj +++ b/docs/orleans/tutorials-and-samples/snippets/minimal/Silo/Silo.csproj @@ -5,10 +5,10 @@ - + - + diff --git a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/Program.cs b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/Program.cs index 62a291c6179c8..7b8eb890acdd8 100644 --- a/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/Program.cs +++ b/docs/standard/commandline/snippets/get-started-tutorial/csharp/Stage3/Program.cs @@ -119,7 +119,7 @@ internal static void DeleteFromFile(FileInfo file, string[] searchTerms) { Console.WriteLine("Deleting from file"); - var lines = File.ReadLines(file.FullName).Where(line => searchTerms.All(s => !line.Contains(s))); + var lines = File.ReadLines(file.FullName).Where(line => searchTerms.All(s => !line.Contains(s))).ToList(); File.WriteAllLines(file.FullName, lines); } internal static void AddToFile(FileInfo file, string quote, string byline) diff --git a/docs/standard/simd.md b/docs/standard/simd.md index c275fc3c1fa2a..c28218df490df 100644 --- a/docs/standard/simd.md +++ b/docs/standard/simd.md @@ -1,121 +1,343 @@ --- -title: SIMD-accelerated types in .NET -description: This article describes SIMD-enabled types in .NET and demonstrates how to use hardware SIMD operations in C# and .NET. +title: Use SIMD and hardware intrinsics in .NET +description: Learn about the layered SIMD support in .NET, from System.Numerics vector types to cross-platform Vector64/128/256/512 APIs, hardware intrinsics, and TensorPrimitives. author: FIVIL ms.author: tagoo -ms.date: 03/26/2026 +ms.date: 07/18/2026 ai-usage: ai-assisted --- -# Use SIMD-accelerated numeric types +# Use SIMD and hardware intrinsics in .NET -SIMD (Single instruction, multiple data) provides hardware support for performing an operation on multiple pieces of data, in parallel, using a single instruction. In .NET, there's set of SIMD-accelerated types under the namespace. SIMD operations can be parallelized at the hardware level. That increases the throughput of the vectorized computations, which are common in mathematical, scientific, and graphics apps. +SIMD (single instruction, multiple data) is hardware support for applying one operation to multiple pieces of data in parallel with a single instruction. Vectorized code processes several values per iteration instead of one, which can greatly increase throughput for the kind of numeric, scientific, graphics, text-processing, and data-parallel work where the same operation repeats over a buffer. The trade-off is added complexity, so it pays off most when the input is large enough and the win is confirmed with measurements. -## .NET SIMD-accelerated types +.NET provides several kinds of SIMD support. Pick the one that matches how much control you need and how much complexity you're willing to take on. -The .NET SIMD-accelerated types include the following types: +## Types of SIMD support in .NET -- The , , and types, which represent vectors with 2, 3, and 4 values. +| API | Namespace | When to use it | +| --- | --- | --- | +| Fixed-purpose vector and matrix types | | Graphics and geometry math with 2-4 element vectors, matrices, quaternions, and planes. | +| `Vector` | | Portable, variable-width vectorization when you don't need per-platform control. | +| `Vector64`, `Vector128`, `Vector256`, `Vector512` | | Cross-platform, fixed-width vectorization with fine-grained control. This is the recommended starting point for new vectorized algorithms. | +| Hardware intrinsics | , , | Specific processor instructions that the higher-level APIs don't expose, for the last bit of performance on a hot path. | +| `TensorPrimitives` | | Ready-made, vectorized math over spans. It does the vectorization for you. | -- Two matrix types, , which represents a 3x2 matrix, and , which represents a 4x4 matrix of values. +Where these APIs overlap, they relate through layers of abstraction. The generic vector types are the foundational interchange types that the other layers pass around, so they're technically the lowest level: the variable-width `Vector`, which grows to whatever width the running hardware supports, and the fixed-width `Vector64` through `Vector512`. The platform-specific hardware intrinsics in , , and operate on those types, each mapping directly to an individual processor instruction. The cross-platform operations exposed on the generic types sit a step above the platform-specific intrinsics, lowering to them for each target. Higher still are the managed APIs that operate over entire buffers—vectorized methods on `Span` and `string`, and —which build on the layers below, so you get SIMD acceleration without hand-writing any of it. The fixed-shape types are domain-specific convenience types for graphics and geometry rather than part of this interchange stack. -- The type, which represents a plane in three-dimensional space using values. +The rest of this article works through these APIs from the highest level to the lowest, then covers testing, benchmarking, and best practices. -- The type, which represents a vector that is used to encode three-dimensional physical rotations using values. +## System.Numerics vector and matrix types -- The type, which represents a vector of a specified numeric type and provides a broad set of operators that benefit from SIMD support. The count of a instance is fixed for the lifetime of an application, but its value depends on the CPU of the machine running the code. +The namespace provides SIMD-accelerated types with a fixed shape: - > [!NOTE] - > The type is not included in .NET Framework. You must install the [System.Numerics.Vectors](https://www.nuget.org/packages/System.Numerics.Vectors) NuGet package to get access to this type. - -The SIMD-accelerated types are implemented in such a way that they can be used with non-SIMD-accelerated hardware or JIT compilers. To determine whether SIMD acceleration is available at runtime, use . If that property returns `true`, at least some APIs use hardware-accelerated SIMD operations. If it returns `false`, no APIs are hardware accelerated. +- , , and represent vectors of 2, 3, and 4 values. +- and represent 3x2 and 4x4 matrices of values. +- represents a plane in three-dimensional space. +- represents a vector used to encode three-dimensional rotations. -## How to use SIMD? +These types map naturally to graphics and geometry, and the runtime accelerates their operations with SIMD instructions where the hardware supports them. The following example adds two vectors: -Before executing custom SIMD algorithms, it's possible to check if the host machine supports SIMD by using , which returns a . This doesn't guarantee that SIMD-acceleration is enabled for a specific type, but is an indicator that it's supported by some types. +:::code language="csharp" source="./snippets/simd/csharp/NumericsExamples.cs" id="SimpleVectorAdd"::: -## Simple vectors +They also expose the common vector math you'd expect, such as dot product, distance, and clamping: -The most primitive SIMD-accelerated types in .NET are , , and types, which represent vectors with 2, 3, and 4 values. The example below uses to add two vectors. +:::code language="csharp" source="./snippets/simd/csharp/NumericsExamples.cs" id="SimpleVectorOps"::: -```csharp -var v1 = new Vector2(0.1f, 0.2f); -var v2 = new Vector2(1.1f, 2.2f); -var vResult = v1 + v2; -``` +The matrix types support matrix math such as transpose and multiplication: + +:::code language="csharp" source="./snippets/simd/csharp/NumericsExamples.cs" id="MatrixMultiply"::: + +## Vector\ + +`Vector` represents a variable-width vector of a primitive numeric type. Its length is fixed for the lifetime of the process, but the value of `Vector.Count` depends on the CPU that runs the code. The Just-In-Time (JIT) compiler treats `Count` as a constant, so loops written against it optimize well. + +`Vector` gives you portable vectorization without per-platform code, at the cost of not knowing the vector width at compile time. The following example computes the element-wise add of two arrays: + +:::code language="csharp" source="./snippets/simd/csharp/NumericsExamples.cs" id="VectorTAdd"::: + +> [!NOTE] +> This example is illustrative. You rarely need to write a loop like this by hand, because [`TensorPrimitives`](#higher-level-math-with-tensorprimitives) already provides accelerated span-based math. This element-wise add is , and reductions such as are provided too. These operations are hardware-accelerated for the element types that `Vector` supports (`Vector.IsSupported`). + +## Check for hardware acceleration + +SIMD-accelerated types work even on hardware or JIT configurations that don't support SIMD, because they fall back to non-accelerated software implementations. To branch on whether acceleration is actually available, check the relevant `IsHardwareAccelerated` property: + +- reports whether `Vector` operations are accelerated. +- , and the `Vector64`/`Vector256`/`Vector512` equivalents, report acceleration for each fixed width. -It's also possible to use .NET vectors to calculate other mathematical properties of vectors such as `Dot product`, `Transform`, `Clamp` and so on. +These properties are turned into constants by the JIT, so the branches you don't take are eliminated and there's no runtime cost to checking them. Don't cache the values; read them directly where you need them. The same applies to the `Count` properties (for example, `Vector128.Count`), which are also JIT-time constants. + +Most operations on an accelerated width are themselves accelerated, but it isn't guaranteed for every operation. For example, floating-point division might be accelerated where integer division isn't. When `Vector256` is accelerated, `Vector128` usually is too, but there's no guarantee, so check each width you use. + +> [!TIP] +> If an operation you need isn't accelerated on a platform you care about, or you'd like a new cross-platform API, file an issue on [dotnet/runtime](https://github.com/dotnet/runtime/issues). The same applies to codegen improvements. + +Not every element type is valid for every vector. `Vector128` and its siblings support the primitive numeric types (`byte`, `sbyte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `nint`, and `nuint`) today, and that set might grow to include other types in the future. Use `Vector128.IsSupported` to determine whether a given `T` is valid, which is especially useful from generic code. + +Types that aren't supported, such as `char` and `bool`, can still be vectorized by reinterpreting the buffer as a supported type of the same size. Use to reinterpret a span—for example, `char` to `ushort`—or the vector's `As` method to reinterpret a vector you already hold. Reinterpretation only changes the type, not the underlying bits, so it's your responsibility to keep the data well-formed: a `bool` must stay `0` or `1`, and a `char` must remain a valid UTF-16 code unit. If a vectorized operation could produce an out-of-range value, take care to normalize the result before writing it back. + +## Cross-platform vectorization with Vector128 + +`Vector128` is the common denominator across every platform that supports vectorization, so it's the best place to start. It holds a 128-bit vector: 16 bytes, 8 shorts, 4 ints/floats, or 2 longs/doubles. -```csharp -var v1 = new Vector2(0.1f, 0.2f); -var v2 = new Vector2(1.1f, 2.2f); -var vResult1 = Vector2.Dot(v1, v2); -var vResult2 = Vector2.Distance(v1, v2); -var vResult3 = Vector2.Clamp(v1, Vector2.Zero, Vector2.One); ``` +------------------------------128-bits--------------------------- +| 64 | 64 | +----------------------------------------------------------------- +| 32 | 32 | 32 | 32 | +----------------------------------------------------------------- +| 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | +----------------------------------------------------------------- +| 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | +----------------------------------------------------------------- +``` + +`Vector256` is twice as wide, and `Vector512` twice again. Not all hardware supports the larger widths, so the examples that follow use `Vector128` for portability. + +Each width has a generic type (`Vector128`) for the data and a non-generic static class () that holds most of the operations, including static factory methods like `Create` and `Load`. Operators such as `+`, `&`, and `<<` are the idiomatic way to express arithmetic and bit operations; prefer them over the named method equivalents to avoid operator-precedence bugs and improve readability. For algorithms that depend on byte order, branch on , which the JIT also folds to a constant. -## Matrix +> [!NOTE] +> On x86/x64, `Vector256` operations are generally treated as two independent 128-bit "lanes". For most element-wise operations this is transparent, but operations that cross lanes (such as shuffles or pairwise/horizontal operations) might behave differently or cost more than the `Vector128` equivalent. Confirm with benchmarks before assuming a wider vector is faster. -, which represents a 3x2 matrix, and , which represents a 4x4 matrix. Can be used for matrix-related calculations. The example below demonstrates multiplication of a matrix to its correspondent transpose matrix using SIMD. +### Lane-crossing operations don't widen for free -```csharp -var m1 = new Matrix4x4( - 1.1f, 1.2f, 1.3f, 1.4f, - 2.1f, 2.2f, 3.3f, 4.4f, - 3.1f, 3.2f, 3.3f, 3.4f, - 4.1f, 4.2f, 4.3f, 4.4f); +Element-wise operations don't care about width: `v1 + v2` is the same per-element result whether `v1` and `v2` are `Vector128` or `Vector256`—widening just processes one more lane's worth of data per instruction. `Add` on `v = [a, b, c, d]` and `w = [e, f, g, h]` always combines same-index elements: -var m2 = Matrix4x4.Transpose(m1); -var mResult = Matrix4x4.Multiply(m1, m2); +``` +v: [ a | b | c | d ] +w: [ e | f | g | h ] + + + + + +r: [a+e|b+f|c+g|d+h] ``` -## Vector\ +Lane-crossing operations don't scale up that simply, because which elements get combined depends on the vector's width. A pairwise reduction combines *adjacent* elements instead of same-index ones, so widening changes what ends up paired together: -The gives the ability to use longer vectors. The count of a instance is fixed, but its value depends on the CPU of the machine running the code. - -The following example demonstrates how to calculate the element-wise sum of two arrays using . - -```csharp -double[] Sum(double[] left, double[] right) -{ - if (left is null) - { - throw new ArgumentNullException(nameof(left)); - } - - if (right is null) - { - throw new ArgumentNullException(nameof(right)); - } - - if (left.Length != right.Length) - { - throw new ArgumentException($"{nameof(left)} and {nameof(right)} are not the same length"); - } - - int length = left.Length; - double[] result = new double[length]; - - // Get the number of elements that can't be processed in the vector - // NOTE: Vector.Count is a JIT time constant and will get optimized accordingly - int remaining = length % Vector.Count; - - for (int i = 0; i < length - remaining; i += Vector.Count) - { - var v1 = new Vector(left, i); - var v2 = new Vector(right, i); - (v1 + v2).CopyTo(result, i); - } - - for (int i = length - remaining; i < length; i++) - { - result[i] = left[i] + right[i]; - } - - return result; -} ``` +v: [ a | b | c | d ] + \_____/ \_____/ +round 1: [ a+b | c+d | a+b | c+d ] + \_________________/ +round 2: [ S | S | S | S ] (S = a+b+c+d) +``` + +That's exactly what a horizontal reduction does: sum a vector's elements with two rounds of pairwise adds. On x86/x64, `Vector128` (4 elements) gets you there with two calls to : + +:::code language="csharp" source="./snippets/simd/csharp/LaneCrossingExample.cs" id="SumVector128"::: + +Extend the same two-call pattern to `Vector256` (8 elements) and it looks right—it isn't. doesn't operate across the whole 256-bit vector; it repeats the pairwise pattern independently within each 128-bit lane. Two rounds gives you the lower lane's sum (elements 0-3) broadcast across the lower lane and the upper lane's sum (elements 4-7) broadcast across the upper lane, not the sum of all eight elements: + +:::code language="csharp" source="./snippets/simd/csharp/LaneCrossingExample.cs" id="SumVector256Naive"::: + +To get the correct total, bridge the lane boundary explicitly: read each lane's partial sum with `GetLower`/`GetUpper` and add them together—`GetLower` and `GetUpper` split a vector into its first and second halves: + +``` +------------------------------128-bits--------------------------- +| LOWER | UPPER | +----------------------------------------------------------------- +| 32 | 32 | 32 | 32 | +----------------------------------------------------------------- +| 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | +----------------------------------------------------------------- +| 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | +----------------------------------------------------------------- +``` + +:::code language="csharp" source="./snippets/simd/csharp/LaneCrossingExample.cs" id="SumVector256"::: + +That extra step is the real cost of crossing lanes. A lane-crossing algorithm doesn't just widen for free the way an element-wise one does—measure before assuming the wider vector wins. + +### Common operations + +`Vector128` and its wider siblings expose a large API surface. You don't need to memorize it—know the categories and look up the details when you need them. Every operation has a software fallback for platforms that can't accelerate it. The following table covers essentially the whole surface. + +| Category | What it does | Representative APIs | +| --- | --- | --- | +| Constants | Predefined constant vectors | `Zero`, `One`, `NegativeOne`, `AllBitsSet`, `Indices`, `SignSequence`, `E`, `Pi`, `Tau`, `Epsilon`, `NaN`, `PositiveInfinity`, `NegativeInfinity`, `NegativeZero` | +| Creation | Broadcast a scalar, set elements, or generate a sequence | `Create`, `CreateScalar`, `CreateScalarUnsafe`, `Create(ReadOnlySpan)`, `CreateSequence`, `CreateGeometricSequence`, `CreateHarmonicSequence`, `CreateAlternatingSequence` | +| Load and store | Move data between memory and a vector | `Load`, `LoadUnsafe`, `LoadAligned`, `LoadAlignedNonTemporal`, `Store`, `StoreUnsafe`, `StoreAligned`, `StoreAlignedNonTemporal`, `CopyTo`, `TryCopyTo` | +| Arithmetic | Element-wise math and reductions | `Add (x + y)`, `Subtract (x - y)`, `Multiply (x * y)`, `Divide (x / y)`, `Negate (-x)`, `AddSaturate`, `SubtractSaturate`, `Abs`, `Sqrt`, `FusedMultiplyAdd`, `Dot`, `Sum` | +| Bit operations | Bitwise logic and shifts | `BitwiseAnd (x & y)`, `BitwiseOr (x \| y)`, `Xor (x ^ y)`, `AndNot (x & ~y)`, `OnesComplement (~x)`, `ShiftLeft (x << n)`, `ShiftRightArithmetic (x >> n)`, `ShiftRightLogical (x >>> n)` | +| Min, max, and clamp | Element-wise minimum, maximum, and range clamping | `Min`, `Max`, `Clamp`, `MinMagnitude`, `MaxMagnitude`, `MinNumber`, `MaxNumber`, `MinMagnitudeNumber`, `MaxMagnitudeNumber` | +| Rounding | Round each element to an integral value | `Ceiling`, `Floor`, `Round`, `Truncate` | +| Math functions | Sign, interpolation, angle, and transcendental helpers | `CopySign`, `Lerp`, `DegreesToRadians`, `RadiansToDegrees`, `Hypot`, `Sin`, `Cos`, `SinCos`, `Asin`, `Exp`, `Log`, `Log2` | +| Comparison | Compare each element; the result is a vector mask, not the `bool` an operator gives | `Equals`, `GreaterThan`, `GreaterThanOrEqual`, `LessThan`, `LessThanOrEqual` | +| Classification | Per-element predicates over the number line, each returning a vector mask | `IsNaN`, `IsFinite`, `IsInfinity`, `IsPositiveInfinity`, `IsNegativeInfinity`, `IsInteger`, `IsEvenInteger`, `IsOddInteger`, `IsNegative`, `IsPositive`, `IsNormal`, `IsSubnormal`, `IsZero` | +| Comparison reductions | Collapse a per-element comparison to a single `bool` | `EqualsAll (x == y)`, `EqualsAny`, `GreaterThanAll`, `GreaterThanAny`, `GreaterThanOrEqualAll`, `GreaterThanOrEqualAny`, `LessThanAll`, `LessThanAny`, `LessThanOrEqualAll`, `LessThanOrEqualAny` | +| Whole-vector predicates | Reduce a vector to a `bool`: whether all, any, or no elements equal a value, or (the `WhereAllBitsSet` forms) have all bits set. Prefer these over converting a mask to an index | `All`, `Any`, `None`, `AllWhereAllBitsSet`, `AnyWhereAllBitsSet`, `NoneWhereAllBitsSet` | +| Search | Count or locate elements by value, or (the `WhereAllBitsSet` forms) set lanes in a mask | `Count`, `IndexOf`, `LastIndexOf`, `CountWhereAllBitsSet`, `IndexOfWhereAllBitsSet`, `LastIndexOfWhereAllBitsSet` | +| Mask to index | Turn a comparison mask into a scalar bitmask and scan it | `ExtractMostSignificantBits` with or | +| Selection | Blend two vectors according to a mask, bit by bit | `ConditionalSelect(x, y, z)`, equivalent to `(y & x) \| (z & ~x)` | +| Conversion | Change numeric type, computing new values (for example, `int` to `float`) | `ConvertToInt32`, `ConvertToInt64`, `ConvertToUInt32`, `ConvertToUInt64`, `ConvertToSingle`, `ConvertToDouble` | +| Widening and narrowing | Split elements into a wider type or pack them into a narrower one | `Widen`, `WidenLower`, `WidenUpper`, `Narrow`, `NarrowWithSaturation` | +| Reinterpretation | Reinterpret the bits as another element type without changing them | `As`, `AsByte`, `AsInt32`, `AsSingle`, and the other `As*` element forms | +| System.Numerics interop | Reinterpret between `Vector128` and the fixed-shape numerics types | `AsVector`, `AsVector2`, `AsVector3`, `AsVector4`, `AsPlane`, `AsQuaternion`, `AsVector128`, `AsVector128Unsafe` | +| Reorder | Rearrange elements by index or interleave two vectors | `Shuffle`, `Reverse`, `Zip`, `ZipLower`, `ZipUpper`, `Unzip`, `UnzipEven`, `UnzipOdd`, `ConcatLowerLower`, `ConcatLowerUpper`, `ConcatUpperLower`, `ConcatUpperUpper` | +| Lane access | Read or replace individual elements and halves, or resize the vector | `GetElement`, `WithElement`, `ToScalar`, `GetLower`, `GetUpper`, `WithLower`, `WithUpper`, `ToVector256` | + +> [!TIP] +> Several operations have `Estimate` and `Native` variants—for example, `MultiplyAddEstimate`, `ClampNative`, `MinNative`, `MaxNative`, `ShuffleNative`, and `ConvertToInt32Native`. They map to a faster hardware instruction that trades some precision or gives up an IEEE edge-case guarantee (such as NaN handling), so reach for them only when a benchmark shows the exact form is the bottleneck and the looser semantics are acceptable. + +> [!NOTE] +> `Vector256.Shuffle` treats its input as a single 256-bit vector, where-as the platform-specific `Avx2.Shuffle` operates as two independent 128-bit lanes. The cross-platform API is the more portable choice, but confirm the behavior you need when porting hand-written intrinsics. + +### Structure the code path + +A vectorized method typically fans out into a path per vector width plus a scalar fallback for small inputs and non-accelerated hardware. To use the largest vector the hardware supports, check the widest vector first and work down: + +:::code language="csharp" source="./snippets/simd/csharp/AdvancedVectorization.cs" id="CodeStructure"::: + +Each width's outer guard combines `Vector128.IsHardwareAccelerated` (a JIT-time constant for whether the platform accelerates that width) with `Vector128.IsSupported` (whether the element type `T` is valid for that width). Inside a supported block, compare the input length against `Count` to choose between the vectorized path and a small-input fallback. The method is generic over `T`, and the `Vector256` and `Vector512` blocks—identical to the `Vector128` block but using the wider type—are shown commented out for brevity. + +There are two distinct fallbacks. A buffer that's too small for even the narrowest vector, but on accelerated hardware, goes to `SumVectorSmall`—an explicit `switch` jump table that handles each possible sub-vector length without a loop: + +:::code language="csharp" source="./snippets/simd/csharp/AdvancedVectorization.cs" id="VectorSmall"::: + +`sizeof(T)` is also a JIT-time constant, so `SumVectorSmall` dispatches on the element width to a table sized for the widest vector's worth of elements—the same approach uses. (When the new memory safety model is enabled, `sizeof(T)` expressions on a type parameter with the `unmanaged` constraint is allowed in safe code.) Only the 4-byte table is shown; the 1-byte, 2-byte, and 8-byte tables share its shape. Its larger cases fold the leftover with a `Vector256` or `Vector128` using two overlapping loads—one from the start, one from the end—so the wider-remainder handling for the omitted `Vector512`/`Vector256` paths lives right in the jump table. The two loads overlap whenever the length isn't an exact multiple of the width, so the tail is masked to the additive identity with `ConditionalSelect` before it's summed. That mask is only needed because addition is non-idempotent; an idempotent operation such as a search could fold the overlapping tail in directly. A buffer on hardware with no vectorization at all falls through to `SumScalar`, an ordinary scalar loop. + +### Loop over the input and handle the remainder + +To process a buffer larger than a single vector, loop over it a vector at a time, then handle the leftover elements that don't fill a full vector. The robust way to handle that tail is to reprocess the last full vector's worth of elements, overlapping some the loop already handled—which avoids a separate scalar epilogue. Whether that overlap needs correcting depends on the operation. + +A **non-idempotent** operation such as a sum would count the overlapping elements twice, so mask them down to the operation's identity before folding them in. Use this when each element must contribute exactly once: + +:::code language="csharp" source="./snippets/simd/csharp/AdvancedVectorization.cs" id="MaskedRemainder"::: + +This version guards the unrolled loop with an `if`, so small payloads skip the four accumulators entirely and fall straight to the remainder. When there's enough data, a `do`/`while` accumulates four vectors per iteration into independent accumulators—which lets the processor pipeline the additions—and combines them pairwise. A `switch` jump table then folds in the remaining zero-to-three full vectors and, in `case 0`, the sub-vector tail: it reuses a full vector preloaded from the end of the buffer, overlapping the elements already processed, and masks that overlap down to the additive identity with `ConditionalSelect` so the tail stays vectorized instead of falling out to a scalar loop. As before, `Vector128.Create` reads `Vector128.Count` elements from the span. The JIT elides the span's bounds check for typical access patterns, so `Create` is a fine default even in a hot loop; `LoadUnsafe` (covered next) is the lower-level alternative for when you walk the buffer by managed reference. For very large inputs, a complete implementation would also align the buffer and use non-temporal loads and stores to avoid evicting useful cache lines—both omitted here and covered fully by `TensorPrimitives`. + +An **idempotent** operation such as searching for a value can reprocess the overlap harmlessly, so it folds the last vector in directly with no mask: + +:::code language="csharp" source="./snippets/simd/csharp/SimpleVectorization.cs" id="VectorizedRemainder"::: + +> [!WARNING] +> Mishandling the remainder is a common source of bugs. A loop that reads past the end of the buffer produces non-deterministic results and can crash. The runtime's test suite uses a `BoundedMemory` helper that places a no-access page immediately after the buffer, so any out-of-bounds read throws an during testing. Always cover the remainder logic, including buffers whose length isn't a multiple of the vector width. + +### Load and store vectors safely + +For most code, `Vector128.Create(span)` and are the simplest way to move data between a span and a vector, and the JIT keeps them efficient. When you need the lower-level load and store—for example, to walk a buffer by managed reference—prefer the and overloads that take a managed reference and an `nuint` element offset. Unlike the pointer-based `Load`/`Store` overloads, they don't require pinning the buffer, and unlike raw reference arithmetic, they don't need you to manually advance a `ref`. Both alternatives are easy to get wrong in ways that introduce garbage-collector holes or access violations. + +So that empty buffers don't throw, get the starting reference from (or for arrays) rather than `ref span[0]`. + +> [!IMPORTANT] +> Offset arithmetic uses unsigned `nuint`. Always check the buffer length before computing an offset such as `buffer.Length - Vector128.Count`. If the buffer is smaller than one vector, that subtraction underflows to a huge value and the loop reads invalid memory. + +## Platform-specific hardware intrinsics + +When a specific processor instruction gives you an edge that the portable APIs don't expose, reach for the hardware intrinsics in , , and . Each intrinsic class has an `IsSupported` property (also a JIT constant) so you can guard the specialized path and fall back to portable code elsewhere: + +:::code language="csharp" source="./snippets/simd/csharp/HardwareIntrinsicsExample.cs" id="HardwareIntrinsics"::: + +The preceding method shows how to light up per-architecture code paths when you want them, but it's deliberately a simple example: you don't actually need it here. The portable expression `(vector & mask) == Vector128.Zero` already lowers to the optimal instruction on each platform (for example, `ptest` on x86/x64), so it's doing the same work as the hand-written branches, just without the complexity. Reach for explicit intrinsics only when a specific instruction measurably beats what the portable APIs generate. + +Hardware intrinsics require a separate implementation per instruction set, so treat them as an optimization for measured hot paths rather than a default. The `Vector128`/`Vector256` APIs already lower to efficient instructions on each platform, and in practice sophisticated per-instruction code doesn't always win. Confirm the difference with a benchmark before committing to the extra maintenance. + +## Higher-level math with TensorPrimitives + +If you need vectorized math over spans and don't want to write the loops yourself, provides a large set of numeric operations—element-wise arithmetic, exponentials, and reductions such as dot product and cosine similarity—that are already vectorized internally. It's available in the [System.Numerics.Tensors](https://www.nuget.org/packages/System.Numerics.Tensors) NuGet package. + +:::code language="csharp" source="./snippets/simd/csharp/TensorPrimitivesExample.cs" id="TensorPrimitives"::: + +For AI and numeric workloads, `TensorPrimitives` often delivers most of the benefit of hand-written SIMD with none of the complexity. + +## Test all code paths + +Because a vectorized method has several code paths, tests need to cover each one: the `Vector256` path, the `Vector128` path, and the scalar path, each with inputs both large enough and too small to benefit. You can vary the input size in tests, but you can't toggle hardware acceleration at the test level. Instead, control it with environment variables before the process starts: + +- Set `DOTNET_EnableAVX2=0` to make return `false`. +- Set `DOTNET_EnableHWIntrinsic=0` to disable intrinsics entirely, so `Vector128`, `Vector64`, and `Vector` all report no acceleration. + +To exercise every path on a single machine, run the test suite once with no overrides, once with `DOTNET_EnableAVX2=0`, and once with `DOTNET_EnableHWIntrinsic=0`. The alternative is running across enough varied hardware to cover them. + +### Instruction-set configuration knobs + +Beyond those two, the runtime recognizes a knob per logical grouping of instruction sets, each prefixed with `DOTNET_`. A single knob can cover several related instruction sets—`EnableAVX2`, for instance, gates AVX2 along with BMI1, BMI2, F16C, FMA, LZCNT, and MOVBE. Setting a knob to `0` disables its whole group and everything layered on top of it. Setting it to `1` (the default for most) allows the group, but the hardware must still actually support it—enabling a knob the current CPU lacks is ignored, so you can only ever narrow what's used, never force an unsupported instruction on and break yourself. `DOTNET_EnableHWIntrinsic=0` is the big hammer—it turns off everything down to the base, so `Vector128`, `Vector64`, and `Vector` all report no acceleration and the code falls to its software path. + +> [!IMPORTANT] +> These are diagnostic tools, meant primarily for testing and validation—exercising each code path, reproducing a hardware-specific issue, or confirming a fallback. They aren't designed for general or production use, and they aren't a stability contract. The following set is what .NET 11 recognizes; earlier releases exposed a different set—the baseline and AVX-512 knobs in particular were reconfigured—so confirm the names against the runtime version you target. +> +> These tools also have limits on what they reach. Because they gate JIT decisions, they don't affect code that was already compiled ahead of time through ReadyToRun or Native AOT, and they don't necessarily affect internal routines the runtime and core libraries use themselves. Treat them as a way to steer your own JIT-compiled code, not a global off switch for an instruction set. + +The base switch and the width caps apply on every architecture: + +| Knob (`DOTNET_` prefix) | Default | Effect | +| --- | --- | --- | +| `EnableHWIntrinsic` | `1` | Master switch for all hardware intrinsics; `0` forces the fully software path. | +| `MaxVectorTBitWidth` | system default | Caps `Vector` to a maximum width in bits; a value below 128 means the system default. | +| `PreferredVectorBitWidth` | system default | Caps the maximum fixed-width vector that reports `IsHardwareAccelerated`, in bits; a value below 128 means the system default. | + +The system default for `MaxVectorTBitWidth` can be narrower than the hardware fully supports, so `Vector` doesn't automatically grow to the widest available vector. For example, `Vector512.IsHardwareAccelerated` can be `true` while `Vector` stays 256-bit; set `DOTNET_MaxVectorTBitWidth=512` to opt `Vector` into the wider width. + +`PreferredVectorBitWidth` caps the maximum vector width that reports `IsHardwareAccelerated`. Lowering it below what the hardware supports flips the wider widths off: on a machine that supports 512-bit vectors, `DOTNET_PreferredVectorBitWidth=256` makes `Vector512.IsHardwareAccelerated` report `false`. It's a general knob, but only x86/x64 offers widths above 128 today, so that's the only place it has an observable effect. + +Each logical grouping of x86/x64 instruction sets also has its own switch: + +| Knob (`DOTNET_` prefix) | Default | Gates | +| --- | --- | --- | +| `EnableAVX` | `1` | AVX and dependents | +| `EnableAVX2` | `1` | AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE, and dependents | +| `EnableAVX512` | `1` | AVX-512 F+BW+CD+DQ+VL and dependents | +| `EnableAVX512BMM` | `1` | AVX-512 BMM | +| `EnableAVX512v2` | `1` | AVX-512 IFMA+VBMI | +| `EnableAVX512v3` | `1` | AVX-512 BITALG+VBMI2+VPOPCNTDQ+VNNI | +| `EnableAVX10v1` | `1` | AVX10.1 | +| `EnableAVX10v2` | `0` | AVX10.2 | +| `EnableAPX` | `0` | APX (extended general-purpose registers) | +| `EnableAES` | `1` | AES, PCLMULQDQ | +| `EnableAVX512VP2INTERSECT` | `1` | AVX-512 VP2INTERSECT | +| `EnableAVXIFMA` | `1` | AVX-IFMA | +| `EnableAVXVNNI` | `1` | AVX-VNNI | +| `EnableAVXVNNIINT` | `1` | VEX AVX-VNNI-INT8 and AVX-VNNI-INT16 | +| `EnableGFNI` | `1` | GFNI | +| `EnableSHA` | `1` | SHA | +| `EnableVAES` | `1` | VAES, VPCLMULQDQ | +| `EnableWAITPKG` | `1` | WAITPKG | +| `EnableX86Serialize` | `1` | X86 SERIALIZE | + +On Arm64, each logical grouping of instruction sets has its own switch: + +| Knob (`DOTNET_` prefix) | Default | Gates | +| --- | --- | --- | +| `EnableArm64Aes` | `1` | AES | +| `EnableArm64Atomics` | `1` | Large System Extensions (LSE) atomics | +| `EnableArm64Crc32` | `1` | CRC32 | +| `EnableArm64Dczva` | `1` | `DC ZVA` cache-zeroing | +| `EnableArm64Dp` | `1` | Dot Product | +| `EnableArm64Rdm` | `1` | Rounding Doubling Multiply Accumulate (RDM) | +| `EnableArm64Sha1` | `1` | SHA1 | +| `EnableArm64Sha256` | `1` | SHA256 | +| `EnableArm64Rcpc` | `1` | Release-Consistent, processor-consistent ordering (RCpc) | +| `EnableArm64Rcpc2` | `1` | RCpc2 | +| `EnableArm64Cssc` | `0` | Common Short Sequence Compression (CSSC) | +| `EnableArm64Sve` | `1` | Scalable Vector Extension (SVE) | +| `EnableArm64Sve2` | `1` | SVE2 | +| `EnableArm64Sha3` | `1` | SHA3 | +| `EnableArm64Sm4` | `1` | SM4 | +| `EnableArm64SveAes` | `1` | SVE AES | +| `EnableArm64SveSha3` | `1` | SVE SHA3 | +| `EnableArm64SveSm4` | `1` | SVE SM4 | + +A knob defaulting to `0` (for example, `EnableAVX10v2` or `EnableArm64Cssc`) gates an instruction set that's still coming online, so it stays off until you opt in. + +## Benchmark to confirm the win + +Vectorization adds complexity, so measure that it pays off before you keep it. Use [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet), and use the same environment variables shown previously to compare the scalar, `Vector128`, and `Vector256` implementations in one run. BenchmarkDotNet's disassembly diagnoser can also emit the generated assembly, which is invaluable when tuning high-performance code. + +A few things to keep in mind: + +- **Larger inputs benefit more.** For small buffers, vectorized code can be slower than scalar code because of setup overhead. Benchmark the input sizes your callers actually use. +- **Speedups are rarely perfect.** A 256-bit vector operating on 32-bit elements won't reliably be 8x faster; memory throughput, alignment, and instruction latency all factor in. +- **Memory alignment affects stability.** Randomized allocation alignment adds noise between runs. You can allocate aligned memory with for stable results, or enable BenchmarkDotNet's memory randomization to observe the full distribution. -## Remarks +## Best practices -SIMD is more likely to remove one bottleneck and expose the next, for example memory throughput. In general the performance benefit of using SIMD varies depending on the specific scenario, and in some cases it can even perform worse than simpler non-SIMD equivalent code. +- Reach for the existing higher-level APIs first. `Span`, `string`, LINQ, `TensorPrimitives`, and the tensor types already accelerate many common operations for you—don't hand-roll what's already optimized and tested. +- Start with `Vector128`; it's accelerated on the broadest set of hardware, and you don't need `Vector256` to get a correct, portable implementation. Add wider widths and hardware intrinsics only for measured hot paths. +- Check `IsHardwareAccelerated` and `Count` directly instead of caching them; the JIT turns them into constants. +- Always handle the loop remainder, and account for overlapping source and destination buffers when storing. +- Write edge-case tests first, then a scalar solution, then express that scalar logic with the vector APIs. +- Test every code path (including access violations) and benchmark realistic input sizes before you commit to the added complexity. + +## See also + +- +- diff --git a/docs/standard/snippets/simd/csharp/AdvancedVectorization.cs b/docs/standard/snippets/simd/csharp/AdvancedVectorization.cs new file mode 100644 index 0000000000000..d20fca1999826 --- /dev/null +++ b/docs/standard/snippets/simd/csharp/AdvancedVectorization.cs @@ -0,0 +1,271 @@ +using System.Diagnostics; +using System.Numerics; +using System.Runtime.Intrinsics; + +namespace SimdSnippets; + +public static class AdvancedVectorization +{ + // + // Sums a buffer, choosing the widest vector the hardware and element type support. + public static T Sum(ReadOnlySpan buffer) + where T : unmanaged, INumberBase + { + // The widest-first order continues with the Vector512 and Vector256 paths, which belong + // here ahead of the Vector128 block below. They're identical to it aside from the wider + // type (for example, Vector512 with Vector512.Create and Vector512.Sum), so they're + // omitted for brevity: + // + // if (Vector512.IsHardwareAccelerated && Vector512.IsSupported) + // { + // if (buffer.Length >= Vector512.Count) + // { + // return SumVector512(buffer); + // } + // return SumVectorSmall(buffer); + // } + // + // if (Vector256.IsHardwareAccelerated && Vector256.IsSupported) + // { + // if (buffer.Length >= Vector256.Count) + // { + // return SumVector256(buffer); + // } + // return SumVectorSmall(buffer); + // } + + if (Vector128.IsHardwareAccelerated && Vector128.IsSupported) + { + if (buffer.Length >= Vector128.Count) + { + return SumVector128(buffer); + } + return SumVectorSmall(buffer); + } + + return SumScalar(buffer); + } + // + + private static T SumScalar(ReadOnlySpan buffer) + where T : unmanaged, INumberBase + { + T result = T.Zero; + foreach (T value in buffer) + { + result += value; + } + return result; + } + + // + // Sums a buffer smaller than the widest vector. The complete "optimal" shape dispatches on the + // element width so each width uses a switch jump table sized to the number of elements that fit + // in the widest vector (63 for byte, 31 for short, 15 for int/float, 7 for long/double). + private static T SumVectorSmall(ReadOnlySpan buffer) + where T : unmanaged, INumberBase + { + // sizeof(T) is a JIT constant, so only the matching branch survives for a given T. + if (sizeof(T) == 4) + { + return SumVectorSmall4(buffer); + } + + // The 1-, 2-, and 8-byte tables share the shape below, sized for their element width. + // They're omitted for brevity, so those widths fall back to a scalar loop here: + // + // if (sizeof(T) == 1) return SumVectorSmall1(buffer); // switch over lengths 0..63 + // if (sizeof(T) == 2) return SumVectorSmall2(buffer); // switch over lengths 0..31 + // if (sizeof(T) == 8) return SumVectorSmall8(buffer); // switch over lengths 0..7 + return SumScalar(buffer); + } + + private static T SumVectorSmall4(ReadOnlySpan buffer) + where T : unmanaged, INumberBase + { + Debug.Assert(sizeof(T) == 4); + Debug.Assert(buffer.Length < Vector512.Count); + + T result = T.Zero; + + // A 4-byte element gives Count == 4/8/16 for Vector128/256/512, so a remainder can be up to + // 15 elements. The larger cases fold the leftover with the widest vector that fits, using two + // overlapping loads (one from the start, one from the end) rather than recursing—the shape + // TensorPrimitives uses. The loads overlap for lengths that aren't an exact multiple of the + // width, so the tail is masked down to the additive identity before it's summed. That mask is + // only needed because addition is non-idempotent; an idempotent operation such as a search + // could fold the overlapping tail in directly. + switch (buffer.Length) + { + // One or two Vector256's worth of data. + case 15: + case 14: + case 13: + case 12: + case 11: + case 10: + case 9: + case 8: + { + Vector256 beg = Vector256.Create(buffer); + Vector256 end = Vector256.Create(buffer.Slice(buffer.Length - Vector256.Count)); + + Vector256 msk = CreateRemainderMask256(buffer.Length - Vector256.Count); + end = Vector256.ConditionalSelect(msk, end, Vector256.Zero); + + result = Vector256.Sum(beg + end); + break; + } + + // One or two Vector128's worth of data. + case 7: + case 6: + case 5: + case 4: + { + Vector128 beg = Vector128.Create(buffer); + Vector128 end = Vector128.Create(buffer.Slice(buffer.Length - Vector128.Count)); + + Vector128 msk = CreateRemainderMask128(buffer.Length - Vector128.Count); + end = Vector128.ConditionalSelect(msk, end, Vector128.Zero); + + result = Vector128.Sum(beg + end); + break; + } + + // Smaller than a single vector: each case falls through to the next, accumulating one + // element per label. + case 3: + { + result += buffer[2]; + goto case 2; + } + + case 2: + { + result += buffer[1]; + goto case 1; + } + + case 1: + { + result += buffer[0]; + goto case 0; + } + + case 0: + { + break; + } + } + + return result; + } + + // Builds a mask whose last `keepLast` lanes are all-bits-set and the rest zero, so an overlapping + // tail load can be folded in without double-counting the lanes the head already covered. + // TensorPrimitives uses an internal table-based helper. The mask is only a bit pattern keyed on + // lane width, so it's built with the same-width integer Indices ([0, 1, 2, ...]) and reinterpreted + // to T: integer comparisons are cheaper than floating-point ones, so a float/double table would + // still compare as int/long rather than in its own element type. + private static Vector256 CreateRemainderMask256(int keepLast) + where T : unmanaged, INumberBase + { + Debug.Assert(sizeof(T) == 4); + + Vector256 firstKept = Vector256.Create(Vector256.Count - keepLast); + return Vector256.GreaterThanOrEqual(Vector256.Indices, firstKept).As(); + } + + private static Vector128 CreateRemainderMask128(int keepLast) + where T : unmanaged, INumberBase + { + Debug.Assert(sizeof(T) == 4); + + Vector128 firstKept = Vector128.Create(Vector128.Count - keepLast); + return Vector128.GreaterThanOrEqual(Vector128.Indices, firstKept).As(); + } + // + + // + // Sums a buffer with an unrolled vector loop plus a masked, jump-table remainder. + private static T SumVector128(ReadOnlySpan buffer) + where T : unmanaged, INumberBase + { + Debug.Assert(Vector128.IsHardwareAccelerated && Vector128.IsSupported); + Debug.Assert(buffer.Length >= Vector128.Count); + + // Preload the last full vector, overlapping the tail. Any sub-vector remainder is folded in + // from here (masked) by case 0 of the switch below, so the loop never falls out to a separate + // scalar tail—the same shape TensorPrimitives uses. + Vector128 end = Vector128.Create(buffer.Slice(buffer.Length - Vector128.Count)); + + // A production implementation would also align the buffer to a vector boundary and, for + // very large inputs, use non-temporal loads/stores so the data doesn't evict useful + // cache lines. Both are omitted here; see TensorPrimitives for a complete treatment. + + Vector128 sum = Vector128.Zero; + + // Only pay for the four independent accumulators when there's enough data to unroll; + // smaller payloads skip straight to the remainder below. Four vectors per iteration lets + // the accumulators pipeline; Vector128.Create reads the first Vector128.Count elements. + if (buffer.Length >= Vector128.Count * 4) + { + Vector128 sum0 = Vector128.Zero; + Vector128 sum1 = Vector128.Zero; + Vector128 sum2 = Vector128.Zero; + Vector128 sum3 = Vector128.Zero; + + do + { + sum0 += Vector128.Create(buffer); + sum1 += Vector128.Create(buffer.Slice(Vector128.Count)); + sum2 += Vector128.Create(buffer.Slice(Vector128.Count * 2)); + sum3 += Vector128.Create(buffer.Slice(Vector128.Count * 3)); + + buffer = buffer.Slice(Vector128.Count * 4); + } + while (buffer.Length >= Vector128.Count * 4); + + // Combine pairwise so the two independent adds can pipeline. + sum = (sum0 + sum1) + (sum2 + sum3); + } + + // Split the remainder into its full vectors and a sub-vector tail. The full vectors fall + // through the jump table; the tail lands in case 0, where the preloaded end is masked so only + // the trailing elements the full vectors didn't already cover are added. + (int blocks, int trailing) = Math.DivRem(buffer.Length, Vector128.Count); + + switch (blocks) + { + case 3: + { + sum += Vector128.Create(buffer.Slice(Vector128.Count * 2)); + goto case 2; + } + + case 2: + { + sum += Vector128.Create(buffer.Slice(Vector128.Count)); + goto case 1; + } + + case 1: + { + sum += Vector128.Create(buffer); + goto case 0; + } + + case 0: + { + Vector128 msk = CreateRemainderMask128(trailing); + sum += Vector128.ConditionalSelect(msk, end, Vector128.Zero); + break; + } + } + + // Horizontally add the lanes into a single scalar. + return Vector128.Sum(sum); + } + // +} diff --git a/docs/standard/snippets/simd/csharp/HardwareIntrinsicsExample.cs b/docs/standard/snippets/simd/csharp/HardwareIntrinsicsExample.cs new file mode 100644 index 0000000000000..743c0bea5652f --- /dev/null +++ b/docs/standard/snippets/simd/csharp/HardwareIntrinsicsExample.cs @@ -0,0 +1,38 @@ +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.Wasm; +using System.Runtime.Intrinsics.X86; + +namespace SimdSnippets; + +public static class HardwareIntrinsicsExample +{ + // + // Illustrates per-platform lightup. The portable '(vector & mask) == Zero' below + // already lowers optimally, so prefer it unless a specific instruction measurably wins. + public static bool AllBitsClear(Vector128 vector, Vector128 mask) + { + if (Sse41.IsSupported) + { + // x86/x64: a single ptest instruction. + return Sse41.TestZ(vector, mask); + } + else if (AdvSimd.Arm64.IsSupported) + { + // Arm64: AND, then reduce the maximum byte across every lane. + Vector128 anded = AdvSimd.And(vector, mask); + return AdvSimd.Arm64.MaxAcross(anded).ToScalar() == 0; + } + else if (PackedSimd.IsSupported) + { + // WebAssembly: AND, then test whether any lane is non-zero. + return !PackedSimd.AnyTrue(PackedSimd.And(vector, mask)); + } + else + { + // Portable fallback for any other platform. + return (vector & mask) == Vector128.Zero; + } + } + // +} diff --git a/docs/standard/snippets/simd/csharp/LaneCrossingExample.cs b/docs/standard/snippets/simd/csharp/LaneCrossingExample.cs new file mode 100644 index 0000000000000..a0d8c9aae7d1c --- /dev/null +++ b/docs/standard/snippets/simd/csharp/LaneCrossingExample.cs @@ -0,0 +1,57 @@ +using System.Diagnostics; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SimdSnippets; + +public static class LaneCrossingExample +{ + // + // Sums all four elements with two rounds of pairwise horizontal adds. + // HorizontalAdd(v, v) on [a, b, c, d] gives [a+b, c+d, a+b, c+d]; a second round + // collapses that to the full sum in every element. + public static float SumVector128(Vector128 v) + { + Debug.Assert(Sse3.IsSupported); + + Vector128 step1 = Sse3.HorizontalAdd(v, v); + Vector128 step2 = Sse3.HorizontalAdd(step1, step1); + + return step2.ToScalar(); + } + // + + // + // The same two-round pattern on Vector256 looks like it should sum all eight + // elements, but Avx.HorizontalAdd repeats the pairwise pattern independently within + // each 128-bit lane. The result holds the lower lane's sum (elements 0-3) broadcast + // across the lower lane and the upper lane's sum (elements 4-7) broadcast across the + // upper lane -- ToScalar only returns the lower lane's partial sum, not the total. + public static float SumVector256Naive(Vector256 v) + { + Debug.Assert(Avx.IsSupported); + + Vector256 step1 = Avx.HorizontalAdd(v, v); + Vector256 step2 = Avx.HorizontalAdd(step1, step1); + + return step2.ToScalar(); + } + // + + // + // Getting the full sum needs an explicit step to cross the lane boundary: read each + // lane's partial sum out with GetLower/GetUpper and add them together. + public static float SumVector256(Vector256 v) + { + Debug.Assert(Avx.IsSupported); + + Vector256 step1 = Avx.HorizontalAdd(v, v); + Vector256 step2 = Avx.HorizontalAdd(step1, step1); + + Vector128 lower = step2.GetLower(); + Vector128 upper = step2.GetUpper(); + + return lower.ToScalar() + upper.ToScalar(); + } + // +} diff --git a/docs/standard/snippets/simd/csharp/NumericsExamples.cs b/docs/standard/snippets/simd/csharp/NumericsExamples.cs new file mode 100644 index 0000000000000..dbe67213b0af3 --- /dev/null +++ b/docs/standard/snippets/simd/csharp/NumericsExamples.cs @@ -0,0 +1,74 @@ +using System.Numerics; + +namespace SimdSnippets; + +public static class NumericsExamples +{ + public static void SimpleVectors() + { + // + Vector2 v1 = Vector2.Create(0.1f, 0.2f); + Vector2 v2 = Vector2.Create(1.1f, 2.2f); + Vector2 sum = v1 + v2; + // + + // + float dot = Vector2.Dot(v1, v2); + float distance = Vector2.Distance(v1, v2); + Vector2 clamped = Vector2.Clamp(v1, Vector2.Zero, Vector2.One); + // + + Console.WriteLine($"sum={sum}, dot={dot}, distance={distance}, clamped={clamped}"); + } + + public static void Matrices() + { + // + Matrix4x4 m1 = Matrix4x4.Create( + 1.1f, 1.2f, 1.3f, 1.4f, + 2.1f, 2.2f, 3.3f, 4.4f, + 3.1f, 3.2f, 3.3f, 3.4f, + 4.1f, 4.2f, 4.3f, 4.4f); + + Matrix4x4 m2 = Matrix4x4.Transpose(m1); + Matrix4x4 product = Matrix4x4.Multiply(m1, m2); + // + + Console.WriteLine($"product.M11={product.M11}"); + } + + // + // Illustrative: element-wise add with Vector. In practice, prefer the already-accelerated + // TensorPrimitives.Add, which is optimized for every Vector.IsSupported element type. + public static double[] Add(double[] left, double[] right) + { + ArgumentNullException.ThrowIfNull(left); + ArgumentNullException.ThrowIfNull(right); + ArgumentOutOfRangeException.ThrowIfNotEqual(right.Length, left.Length); + + double[] result = new double[left.Length]; + + int i = 0; + + // Vector.Count is a JIT-time constant, so the compiler optimizes the loop bound. + int lastVectorStart = left.Length - Vector.Count; + + for (; i <= lastVectorStart; i += Vector.Count) + { + Vector v1 = Vector.Create(left.AsSpan(i)); + Vector v2 = Vector.Create(right.AsSpan(i)); + (v1 + v2).CopyTo(result, i); + } + + // Process any remaining elements that don't fill a full vector. + // Simplified for illustration: a scalar tail isn't optimal. A vectorized + // remainder that reprocesses the last full vector avoids the per-element loop. + for (; i < left.Length; i++) + { + result[i] = left[i] + right[i]; + } + + return result; + } + // +} diff --git a/docs/standard/snippets/simd/csharp/Program.cs b/docs/standard/snippets/simd/csharp/Program.cs new file mode 100644 index 0000000000000..6ad4ca6ce057f --- /dev/null +++ b/docs/standard/snippets/simd/csharp/Program.cs @@ -0,0 +1,66 @@ +using System.Numerics; +using System.Linq; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SimdSnippets; + +// Verification runner. Not part of the published snippets. + +Console.WriteLine($"Vector.IsHardwareAccelerated: {Vector.IsHardwareAccelerated}"); +Console.WriteLine($"Vector128.IsHardwareAccelerated: {Vector128.IsHardwareAccelerated}"); +Console.WriteLine($"Vector256.IsHardwareAccelerated: {Vector256.IsHardwareAccelerated}"); +Console.WriteLine($"Vector512.IsHardwareAccelerated: {Vector512.IsHardwareAccelerated}"); +Console.WriteLine($"Vector.Count: {Vector.Count}"); +Console.WriteLine(); + +NumericsExamples.SimpleVectors(); +NumericsExamples.Matrices(); + +double[] left = [1, 2, 3, 4, 5, 6, 7]; +double[] right = [10, 20, 30, 40, 50, 60, 70]; +Console.WriteLine($"Vector Add: [{string.Join(", ", NumericsExamples.Add(left, right))}]"); +Console.WriteLine(); + +int[] numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +double[] reals = [1.5, 2.5, 3.0, 4.0, 5.0]; +Console.WriteLine($"Sum(1..10) = {AdvancedVectorization.Sum(numbers)}"); +Console.WriteLine($"Sum(reals) = {AdvancedVectorization.Sum(reals)}"); +Console.WriteLine($"Sum(small) = {AdvancedVectorization.Sum(new int[] { 1, 2, 3 })}"); +Console.WriteLine($"Sum(1..37) = {AdvancedVectorization.Sum(Enumerable.Range(1, 37).ToArray())}"); +Console.WriteLine($"Contains(7) = {SimpleVectorization.Contains(numbers, 7)}"); +Console.WriteLine($"Contains(42) = {SimpleVectorization.Contains(numbers, 42)}"); +Console.WriteLine($"Contains(small,2) = {SimpleVectorization.Contains(new int[] { 1, 2, 3 }, 2)}"); +Console.WriteLine($"Contains(small,9) = {SimpleVectorization.Contains(new int[] { 1, 2, 3 }, 9)}"); +Console.WriteLine($"Contains(empty) = {SimpleVectorization.Contains(Array.Empty(), 1)}"); +Console.WriteLine(); + +byte[] ascii = "Hello, SIMD!"u8.ToArray(); +byte[] notAscii = [0x48, 0x65, 0xFF, 0x6C]; +Console.WriteLine($"IsAscii(ascii) = {SimpleVectorization.IsAscii(ascii)}"); +Console.WriteLine($"IsAscii(notAscii) = {SimpleVectorization.IsAscii(notAscii)}"); +Console.WriteLine(); + +Vector128 highBits = Vector128.Create(0b_1000_0000); +Console.WriteLine($"AllBitsClear(zero, highBits) = {HardwareIntrinsicsExample.AllBitsClear(Vector128.Zero, highBits)}"); +Console.WriteLine($"AllBitsClear(0xFF.., highBits) = {HardwareIntrinsicsExample.AllBitsClear(Vector128.AllBitsSet, highBits)}"); +Console.WriteLine(); + +float[] a = [1, 2, 3, 4]; +float[] b = [5, 6, 7, 8]; +float[] c = [1, 1, 1, 1]; +Console.WriteLine($"MultiplyAdd = [{string.Join(", ", TensorPrimitivesExample.MultiplyAdd(a, b, c))}]"); +Console.WriteLine($"CosineSimilarity(a, a) = {TensorPrimitivesExample.CosineSimilarity(a, a)}"); +Console.WriteLine(); + +if (Sse3.IsSupported) +{ + Vector128 v128 = Vector128.Create(1f, 2f, 3f, 4f); + Console.WriteLine($"SumVector128 = {LaneCrossingExample.SumVector128(v128)}"); +} + +if (Avx.IsSupported) +{ + Vector256 v256 = Vector256.Create(1f, 2f, 3f, 4f, 10f, 20f, 30f, 40f); + Console.WriteLine($"SumVector256Naive = {LaneCrossingExample.SumVector256Naive(v256)}"); + Console.WriteLine($"SumVector256 = {LaneCrossingExample.SumVector256(v256)}"); +} diff --git a/docs/standard/snippets/simd/csharp/SimdSnippets.csproj b/docs/standard/snippets/simd/csharp/SimdSnippets.csproj new file mode 100644 index 0000000000000..4df2fd22f5768 --- /dev/null +++ b/docs/standard/snippets/simd/csharp/SimdSnippets.csproj @@ -0,0 +1,15 @@ + + + + Exe + net11.0 + enable + enable + preview + + + + + + + diff --git a/docs/standard/snippets/simd/csharp/SimpleVectorization.cs b/docs/standard/snippets/simd/csharp/SimpleVectorization.cs new file mode 100644 index 0000000000000..8696c076d72c3 --- /dev/null +++ b/docs/standard/snippets/simd/csharp/SimpleVectorization.cs @@ -0,0 +1,87 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SimdSnippets; + +public static class SimpleVectorization +{ + // + // Idempotent search that re-processes the final vector instead of a scalar loop. + public static bool Contains(ReadOnlySpan buffer, int searched) + { + Debug.Assert(Vector128.IsHardwareAccelerated); + + Vector128 values = Vector128.Create(searched); + ReadOnlySpan remaining = buffer; + + while (remaining.Length >= Vector128.Count) + { + if (Vector128.EqualsAny(Vector128.Create(remaining), values)) + { + return true; + } + remaining = remaining.Slice(Vector128.Count); + } + + if (remaining.IsEmpty) + { + return false; + } + + // A partial vector remains. When the buffer holds at least one full vector, + // re-check the last one (overlapping the tail); otherwise scan the few elements directly. + if (buffer.Length >= Vector128.Count) + { + Vector128 tail = Vector128.Create(buffer.Slice(buffer.Length - Vector128.Count)); + return Vector128.EqualsAny(tail, values); + } + + foreach (int value in remaining) + { + if (value == searched) + { + return true; + } + } + + return false; + } + // + + public static bool IsValidAscii(byte value) => (value & 0b1000_0000) == 0; + + // Returns true when every byte in the vector is a valid ASCII value (0-127). + // Anding with the high bit and comparing to zero avoids a movemask extraction. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidAscii(Vector128 vector) => + (vector & Vector128.Create(0b1000_0000)) == Vector128.Zero; + + public static bool IsAscii(ReadOnlySpan buffer) + { + ReadOnlySpan remaining = buffer; + + if (Vector128.IsHardwareAccelerated) + { + while (remaining.Length >= Vector128.Count) + { + if (!IsValidAscii(Vector128.Create(remaining))) + { + return false; + } + remaining = remaining.Slice(Vector128.Count); + } + } + + // Scalar path handles the remainder and short or non-accelerated inputs. + foreach (byte value in remaining) + { + if (!IsValidAscii(value)) + { + return false; + } + } + + return true; + } +} diff --git a/docs/standard/snippets/simd/csharp/TensorPrimitivesExample.cs b/docs/standard/snippets/simd/csharp/TensorPrimitivesExample.cs new file mode 100644 index 0000000000000..b3c3e34b45f17 --- /dev/null +++ b/docs/standard/snippets/simd/csharp/TensorPrimitivesExample.cs @@ -0,0 +1,23 @@ +using System.Numerics.Tensors; + +namespace SimdSnippets; + +public static class TensorPrimitivesExample +{ + // + // Computes result = (left * right) + addend over the whole span, vectorized internally. + public static float[] MultiplyAdd(float[] left, float[] right, float[] addend) + { + float[] result = new float[left.Length]; + + TensorPrimitives.Multiply(left, right, result); + TensorPrimitives.Add(result, addend, result); + + return result; + } + + // Higher-level reductions are available too. + public static float CosineSimilarity(float[] left, float[] right) => + TensorPrimitives.CosineSimilarity(left, right); + // +} diff --git a/docs/zone-pivot-groups.yml b/docs/zone-pivot-groups.yml index 1225143c188f2..f61b42a3d070e 100644 --- a/docs/zone-pivot-groups.yml +++ b/docs/zone-pivot-groups.yml @@ -176,8 +176,6 @@ groups: title: MEAI targets prompt: Choose a host service for your AI models pivots: - - id: github-models - title: GitHub Models - id: azure-openai title: Azure OpenAI - id: openai diff --git a/samples/snippets/csharp/VS_Snippets_CLR/HowToEncryptXMLElementX509/cs/encryptxml.csproj b/samples/snippets/csharp/VS_Snippets_CLR/HowToEncryptXMLElementX509/cs/encryptxml.csproj index ba8fb029b6251..9a802b21d926e 100644 --- a/samples/snippets/csharp/VS_Snippets_CLR/HowToEncryptXMLElementX509/cs/encryptxml.csproj +++ b/samples/snippets/csharp/VS_Snippets_CLR/HowToEncryptXMLElementX509/cs/encryptxml.csproj @@ -7,7 +7,7 @@ - +