Edit

External settings providers

The ordinary settings API is stable. External settings providers are experimental, require opting in to VSEXTPREVIEW_SETTINGS_EXTERNAL, and may change in a later SDK. See Experimental APIs ↗ for guidance on opting in. Ordinary settings do not require VSEXTPREVIEW_SETTINGS.

Back settings with an external store

An ExternalSettingsProvider supplies values from a store your extension owns instead of using the normal settings store. Its ExternalSettingsProviderConfiguration declares a category and externally backed settings that Visual Studio can display without loading the extension merely to discover their metadata.

The ExternalSettingsSample ↗ has one Greeting setting backed by an in-memory store. Its complete provider demonstrates the VSEXTPREVIEW_SETTINGS_EXTERNAL opt-in in that file:

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#pragma warning disable VSEXTPREVIEW_SETTINGS_EXTERNAL // External settings providers are experimental in this SDK.

namespace ExternalSettingsSample;

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Settings;

[VisualStudioContribution]
internal sealed class SampleExternalSettingsProvider : ExternalSettingsProvider
{
    private const string GreetingId = "greeting";
    private const string InvalidGreetingMessage = "Enter a greeting of 1 to 24 non-whitespace characters.";
    private readonly SampleSettingsStore store;

    public SampleExternalSettingsProvider(SampleSettingsStore store)
    {
        this.store = store;
        this.store.Changed += this.OnStoreChanged;
    }

    [VisualStudioContribution]
    internal static SettingCategory Category { get; } = new("externalSettingsSample", "%ExternalSettingsSample.Category.DisplayName%")
    {
        Description = "%ExternalSettingsSample.Category.Description%",
    };

    /// <inheritdoc/>
    protected override ExternalSettingsProviderConfiguration ExternalSettingsProviderConfiguration => new(
        "memoryStore",
        "%ExternalSettingsSample.Provider.DisplayName%",
        Category,
        [
            new ExternalSetting.String(GreetingId, "%ExternalSettingsSample.Greeting.DisplayName%")
            {
                Description = "%ExternalSettingsSample.Greeting.Description%",
                GetValue = (identifier, cancellationToken) =>
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    return Task.FromResult(ExternalSettingOperationResult<string>.Success(this.store.Greeting));
                },
                SetValue = (value, identifier, cancellationToken) =>
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    if (!IsValid(value))
                    {
                        return Task.FromResult(ExternalSettingOperationResult.Failure(InvalidGreetingMessage, ExternalSettingsErrorScope.SingleSettingOnly, isTransient: false));
                    }

                    this.store.SetGreeting(value);
                    return Task.FromResult(ExternalSettingOperationResult.Success());
                },
                Validate = (value, identifier, cancellationToken) =>
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    IReadOnlyList<SettingMessage> messages = IsValid(value)
                        ? []
                        : [new SettingMessage(InvalidGreetingMessage) { Severity = SettingMessageSeverity.Error }];
                    return Task.FromResult(messages);
                },
            },
        ])
    {
        Description = "%ExternalSettingsSample.Provider.Description%",
        BackingStoreDescription = "%ExternalSettingsSample.Provider.BackingStoreDescription%",
    };

    /// <inheritdoc/>
    protected override void Dispose(bool isDisposing)
    {
        if (isDisposing)
        {
            this.store.Changed -= this.OnStoreChanged;
        }

        base.Dispose(isDisposing);
    }

    private static bool IsValid(string value) => !string.IsNullOrWhiteSpace(value) && value.Length <= 24;

    private void OnStoreChanged() => this.NotifySettingValuesChanged([GreetingId]);
}

The provider uses four callbacks and notifications to keep the settings UI and store consistent:

  1. GetValue reads the current greeting and returns ExternalSettingOperationResult<string>.Success.
  2. Validate returns a SettingMessage with error severity for blank or overlong input. SetValue also rejects invalid writes with ExternalSettingOperationResult.Failure; it never reports an invalid write as successful.
  3. A successful SetValue changes the backing store. Changes made outside the settings UI raise the store's Changed event, which calls NotifySettingValuesChanged so Visual Studio refreshes the greeting.
  4. Dispose removes the store event handler when the provider is no longer needed.

A provider must return explicit failures for unavailable reads and writes, and optionally notify Visual Studio when its backing values change.