| Comments

Have you heard about .NET Aspire yet? If not, go read, then maybe watch. It’s okay I’ll wait.

Ok, great now that you have some grounding, I’m going to share some tips time-to-time of things that I find delightful that may not be obvious.  In this example I’m using the default .NET Aspire application template and added an ASP.NET Web API with enlisting into the orchestration. What does that mean exactly? Well the AppHost project (orchestrator) now has a reference to the project like so:

var builder = DistributedApplication.CreateBuilder(args);

builder.AddProject<Projects.WebApplication1>("webapplication1");

builder.Build().Run();

When I run the AppHost it launches all my services, etc. Yes this is a VERY simple case and only one service…I’m here to make a point, stay with me.

If in my service I add some Aspire components they may come with their own configuration information. Things like connection strings or configuration options for the components. A lot of times these will result in environment variables at deploy time that the components will read. You can see this if you run and inspect the environment variables of the app:

Screenshot of .NET Aspire dashboard environment variables

But what if I have a configuration/variable that I need to set that isn’t coming from a component? I want that to be a part of the application model so that the orchestrator puts things in the right places, but also deployment tooling is aware of my whole config needs. No problem, here’s a quick tip if you haven’t discovered it yet!

I want a config value in my app as MY_ENV_CONFIG_VAR…a very important variable. It is a value my API needs as you can see in this super important endpoint:

app.MapGet("/somerandomconfigvar", () =>
{
    var config = builder.Configuration.GetValue<string>("MY_ENV_CONFIG_VAR");
    return config;
});

How can I get this in my Aspire environment so the app model is aware, deployment manifests are aware, etc. Easy. In the AppHost change your AddProject line to add a WithEnvironment() call specifying the variable/value to set. Like this:

var builder = DistributedApplication.CreateBuilder(args);

builder.AddProject<Projects.WebApplication1>("webapplication1")
    .WithEnvironment("MY_ENV_CONFIG_VAR", "Hello world!");

builder.Build().Run();

Now when I launch the orchestrator runs all my services and adds them to the environment variables for that app:

Screenshot of .NET Aspire dashboard environment variables

And when I produce a deployment manifest, that information is stamped as well for deployment tools to reason with and set in their configuration way.

{
  "resources": {
    "webapplication1": {
      "type": "project.v0",
      "path": "..\\WebApplication1\\WebApplication1.csproj",
      "env": {
        "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
        "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
        "MY_ENV_CONFIG_VAR": "Hello world!"
      },
      "bindings": {
        "http": {
          "scheme": "http",
          "protocol": "tcp",
          "transport": "http"
        },
        "https": {
          "scheme": "https",
          "protocol": "tcp",
          "transport": "http"
        }
      }
    }
  }
}

Pretty cool, eh? Anyhow, just a small tip to help you on your .NET Aspire journey.

| Comments
Recently, the .NET team released a starter Codespaces definition for .NET.  There is a great narrated overview of this and the benefit, uses, etc. by the great James Montemagno you can watch here:

Unbelievable Instant .NET Development Setup. It is available when you visit https://github.com/codespaces and you can start using it immediately.

Screenshot of Codespaces Quickstarts

Codespaces are built off of the devcontainer mechanism, which allows you to define the environment in a bunch of different ways using container images or just a devcontainer image.  I won’t go through all the options you can do with devcontainers, but will share the anatomy of this and what I like about it.

NOTE: If you don’t know what Development Containers are, you can read about them here https://containers.dev/

Throughout this post I’ll be referring to snippets of the definition but you can find the FULL definition here: github/dotnet-codespaces.

Base Image

Let’s start with the base image. This is the starting point of the devcontainer, the OS, and pre-configurations built-in, etc. You can use a Dockerfile definition or a pre-defined container image. I think if you have everything bundled nicely in an existing container image in a registry, start there. Just so happens, .NET does this and has nice images with the SDK already in them, so let’s use that!

{
    "name": ".NET in Codespaces",
    "image": "mcr.microsoft.com/dotnet/sdk:8.0",
    ...
}
  

This uses the definition from our own container images defined here: https://hub.docker.com/_/microsoft-dotnet-sdk/. Again this allows us a great/simple starting point.

Features

In the devcontainer world you can define ‘features’ which are like little extensions someone else has done to make it easy to add/inject into the base image. One aspect of adding things can be done through pre/post scripts, but if someone has created a ‘feature’ in the devcontainer world, this makes it super easy as you delegate that setup to this feature owner. For this image we’ve added a few features:

{
    "name": ".NET in Codespaces",
    "image": "mcr.microsoft.com/dotnet/sdk:8.0",
    "features": {
        "ghcr.io/devcontainers/features/docker-in-docker:2": {},
        "ghcr.io/devcontainers/features/github-cli:1": {
            "version": "2"
        },
        "ghcr.io/devcontainers/features/powershell:1": {
            "version": "latest"
        },
        "ghcr.io/azure/azure-dev/azd:0": {
            "version": "latest"
        },
        "ghcr.io/devcontainers/features/common-utils:2": {},
        "ghcr.io/devcontainers/features/dotnet:2": {
            "version": "none",
            "dotnetRuntimeVersions": "7.0",
            "aspNetCoreRuntimeVersions": "7.0"
        }
    },
    ...
}

So here we see that the following are added:

  • Docker in docker – helps us use other docker-based features
  • GitHub CLI – why not, you’re using GitHub so this adds some quick CLI-based commands
  • PowerShell – an alternate shell that .NET developers love
  • AZD – the Azure Developer CLI which helps with quick configuration and deployment to Azure
  • Common Utilities – check out the definition for more info here
  • .NET features – even though we are using a base image, in this case .NET 8, there may be additional runtimes we need so we can use this to bring in more. In this case this is needed for one of our extensions customizations that need the .NET 7 runtime.

This enables the base image to append additional functionality when this devcontainer is used.

Extras

You can configure more extras through a few more properties like customizations (for environments) and pre/post commands.

Customizations

The most common used configuration of this section is to bring in extensions for VS Code. Since Codespaces default uses VS Code, this is helpful and also carries forward if you use VS Code locally with devcontainers (which you can do!).

{
    "name": ".NET in Codespaces",
    ...
    "customizations": {
        "vscode": {
            "extensions": [
                "ms-vscode.vscode-node-azure-pack",
                "github.vscode-github-actions",
                "GitHub.copilot",
                "GitHub.vscode-github-actions",
                "ms-dotnettools.vscode-dotnet-runtime",
                "ms-dotnettools.csdevkit",
                "ms-dotnetools.csharp"
            ]
        }
    },
    ...
}

In this snippet we see that some VS Code definitions will be installed for us to get started quickly:

  • Azure Extensions – a set of Azure extensions to help you quickly work with Azure when ready
  • GitHub Actions – view your repo’s CI/CD activity
  • Copilot – AI-assisted code development
  • .NET Runtime – this helps with any runtime acquisitions needed by activity or other extensions
  • C#/C# Dev Kit – extensions for C# development to make you more productive in the editor

It’s a great way to configure your dev environment to be ready to start when you use devcontainers without spending time hunting down extensions again.

    Commands

    Additionally you can do some post-create commands that may be used to warm-up environments, etc. An example here:

    {
        "name": ".NET in Codespaces",
        ...
        "forwardPorts": [
            8080,
            8081
        ],
        "postCreateCommand": "cd ./SampleApp && dotnet restore",
        ...
    }
    

    This is used to get the sample source ready to use immediately by restoring dependencies or other commands, in this case running the restore command on the sample app.

    Summary

    I am loving devcontainers. Every time I work on a new repository or anything I’m now looking first for a devcontainer to help me quickly get started. For example, I recently explored a Go app/repo and don’t have any of the Go dev tools on my local machine and it didn’t matter. The presence of a devcontainer allowed me to immediately get started with the repo with the dependencies and tools and let me get comfortable. And portable as I can navigate from machine-to-machine with Codespaces and have the same setup needed by using devcontainers!

    Hope this little insight helps.  Check out devcontainers and if you are a repo owner, please add one to your Open Source project if possible!

    | Comments

    I LOVE GitHub Actions! I’ve written about this a lot and how I’ve ‘seen the light’ with regard to ensuring CI/CD is a part of any/every project from the start. That said I’m also a HUGE Visual Studio fan/user.  I like having everything as much as possible at my fingertips in my IDE and for most basic things not have to leave VS to do those things.  Because of this I’ve created GitHub Actions for Visual Studio extension that installs right into Visual Studio 2022 and gives you immediate insight into your GitHub Actions environment. 

    Screenshot of light/dark mode of extension

    Like nearly every one of my projects it starts as completely selfish reasons and tailored to my needs. I spend time doing this in some reserved learning time and the occasional time where my family isn’t around and/or it’s raining and I can’t be on my bike LOL. That said, it may not meet your needs, and that’s okay.

    With that said, let me introduce you to this extension…

    How to launch it

    First you’ll need to have a project/solution open that is attached to GitHub.com and you have the necessary permissions to view this information. The extension looks for GitHub credentials to use interacting with your Windows Credential manager. From VS Solution Explorer, right-click on a project or solution and navigate to the “GitHub Actions” menu item.  This will open a new tool window and start querying the repository and actions for more information.  There is a progress indicator that will show when activity is happening.  Once complete you’ll have a new tool window you can dock anywhere and it will show a few things for you, let’s take a look at what those are.

    Categories

    In the tool window there are 4 primary areas to be aware of:

    Screenshot of the tool window annotated with 4 numbers

    First in the area marked #1 is a small toolbar.  The toolbar has two buttons, one to refresh the data should you need to manually do so for any reason. The second is a shortcut to the repository’s Actions section on GitHub.com.

    Next the #2 area is a tree view of the current branch you have open and workflow runs that targeted that.  It will first show executed (or in-progress) workflow runs, and then you can expand it to see the jobs and steps of each job.  At the ‘leaf’ node of the step you can double-click (or right-click for a menu) and it will open the log for that step on GitHub.com directly.

    The #3 area is a list of the Workflows in your repository by named definition. This is helpful just to see a list of them, but also you can right-click on them and “run” a workflow which triggers a dispatch call to that workflow to execute!

    Finally the #4 area is your Environments and Secrets. Right now Environments just shows you a list of any you have, but not much else. Secrets are limited to Repository Secrets only right now and show you a list and when the secret was last updated.  You can right-click on the Secrets node to add another or double-click on an existing one to edit.  This will launch a quick modal dialog window to capture the secret name/value and upon saving, write to your repository and refresh this list.

    Options

    There are a small set of options you can configure for the extension:

    Screenshot of extension options

    The following can be set:

    • Max Runs (Default=10): this is the maximum number of Workflow Runs to retrieve
    • Refresh Active Jobs (Default=False): if True, this will refresh the Workflow Runs list when any job is known to be in-progress
    • Refresh Interval (Default=5): This is a number in seconds you want to poll for an update on in-progress jobs.

    Managing Workflows

    Aside from viewing the list there are a few other things you can do using the extension:

    • Hover over the Run to see details of the final conclusion state, how it was triggered, the total time for the run, and what GitHub user triggered the run
    • If a run is in-progress, right-click on the run and you can choose to Cancel, which will attempt to send a cancellation to stop the run at whatever step it is in
    • On the steps nodes you can double-click or right-click and choose to view logs.  This will launch your default browser to the location of the step log for that item
    • From the Workflows list, you can right-click on a name of a Workflow and choose “Run Workflow” which will attempt to signal to run the start a run for that Workflow

    Managing Secrets

    Secrets right now are limited to Repository Secrets only.  This is due to a limitation the Octokit library this extension uses.  If you are using Environment Secrets you will not be able to manage them from here. 

    Screenshot of modal dialog for secret editing

    Otherwise:

    • From the Repository Secrets node you can right-click and Add Secret which will launch a modal dialog to supply a name/value for a new secret. Clicking save will persist this to your repo and refresh the list.
    • From an existing secret you can double-click it or right-click and choose ‘Edit’ and will launch the same modal dialog but just enables you to edit the value only.
    • To delete a secret, right-click and choose delete. This is irreversible, so be sure you want to delete!

    Get Started and Log Issues

    To get started, you simply can navigate to the link on the marketplace and click install or use the Extension Manager in Visual Studio and search for “GitHub Actions” and install it.  If you find any issues, the source is available on my GitHub at timheuer/GitHubActionsVS but also would appreciate that you could log an Issue if you find it not working for you.  Thanks for trying it out and I hope it is helpful for you as it is for me.

    | Comments

    As I’ve been working more with Visual Studio Code lately, I wanted to explore more about the developer experience and some of the more challenging areas around customization.  VS Code has a great extensibility model and a TON of UI points for you to integrate.  In the C# Dev Kit we’ve not yet had the need to introduce any custom UI in any views or other experiences that are ‘pixels’ on the screen for the user…pretty awesome extensibility. One area that doesn’t have default UI is the non-text editors. Something that you want to do fully custom in the editor space. For me, I wanted to see what this experience was so I went out to create a small custom editor. I chose to create a ResX editor for the simplest case as ResX is a known-schema based structure that could easily be serialized/de-serialized as needed.

    NOTE: This is not an original idea. There are existing extensions that do ResX editing in different ways. With nearly every project that I set out with, it starts as a learning/selfish reasons…and also selfish scope. Some of the existing ones had expanded features I felt unnecessary and I wanted a simple structure. They are all interesting and you should check them out. I’m no way claiming to be ‘best’ or first-mover here, just sharing my learning path.

    With that said, I’m pleased with what I learned and the result, which is an editor that ‘fits in’ with the VS Code UX and achieves my CRUD goal of editing a ResX file:

    Screenshot of ResX Editor and Viewer

    With that, here’s what I’ve learned a bit…

    Custom Editors and UI

    There are a lot of warnings in the Custom Editor API docs of making sure you really need a custom editor, but point to the value of what they can provide for previewing/WYSIWYG renderings of documents. But they point to the fact that you will likely be using a webview and thus be fully responsible for your UI.  In the end you are owning the UI that you are drawing. For me, I’m not a UI designer, so I rely on others/toolkits to do a lot of heavy lifting. The examples I saw out there (and oddly enough the custom editor sample) don’t match the VS Code UX at all and I didn’t like that. I actually found it odd that the sample took such an extreme approach to the editor (cat paw drawings) rather than show a more realistic data-focused scenario on a known file format.

    Luckily the team provides the Webview UI Toolkit for Visual Studio, a set of components that match the UX of VS Code and adhere to the theming and interaction models. It’s excellent and anyone doing custom UI in VS Code extensions should start using this immediately. Your extension will feel way more professional and at home in the standard VS Code UX.  My needs were fairly simple and I wanted to show the ResX (which is XML format) in a tabular format. The toolkit has a data-grid that was perfect for the job…mostly. But let’s start with the structure.

    Most of the editor is in a provider (per the docs) and that’s where you implement a CustomTextEditorProvider which provides a register and resolveCustomTextEditor commands. Register does what you think, register’s your editor into the ecosystem, using the metadata from package.json about what file types/languages will trigger your editor.  Resolve is where you start providing your content. It provides you with a Webview panel where you put your initial content. Mine was a simple grid:

    private _getWebviewContent(webview: vscode.Webview) {
      const webviewUri = webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'out', 'webview.js'));
      const nonce = getNonce();
      const codiconsUri = webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'media', 'codicon.css'));
      const codiconsFont = webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'media', 'codicon.ttf'));
    
      return /*html*/ `
                <!DOCTYPE html>
                <html lang="en">
                  <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <meta
                      http-equiv="Content-Security-Policy"
                      content="default-src 'none'; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}'; style-src ${webview.cspSource} 'nonce-${nonce}'; style-src-elem ${webview.cspSource} 'unsafe-inline'; font-src ${webview.cspSource};"
                    />
                    <link href="${codiconsUri}" rel="stylesheet" nonce="${nonce}">
                  </head>
                  <body>
                    <vscode-data-grid id="resource-table" aria-label="Basic" generate-header="sticky" aria-label="Sticky Header"></vscode-data-grid>
                    <vscode-button id="add-resource-button">
                      Add New Resource
                      <span slot="start" class="codicon codicon-add"></span>
                    </vscode-button>
                    <script type="module" nonce="${nonce}" src="${webviewUri}"></script>
                  </body>
                </html>
              `;
    }
    }
    

    This serves as the HTML ‘shell’ and then the actual interaction is via the webview.js you see being included. Some special things here or just how it includes the correct link to the js/css files I need but also notice the Content-Security-Policy. That was interesting to get right initially but it’s a recommendation solid meta tag to include (otherwise console will spit out warnings to anyone looking).  The webview.js is basically any JavaScript I needed to interact with my editor. Specifically this uses the registration of the Webview UI Toolkit and converts the resx to json and back (using the npm library resx). Here’s a snippet of that code in the custom editor provider that basically updates the document to JSON format as it changes:

    private async updateTextDocument(document: vscode.TextDocument, json: any) {
    
      const edit = new vscode.WorkspaceEdit();
    
      edit.replace(
        document.uri,
        new vscode.Range(0, 0, document.lineCount, 0),
        await resx.js2resx(JSON.parse(json)));
      return vscode.workspace.applyEdit(edit);
    }
    

    So that gets the essence of the ‘bones’ of the editor that I needed. Once I had the data then a function in the webview.js can ‘bind’ the data to the vscode-data-grid supplying the column names + data easily and just set as the data rows quickly (lines 20,21):

    function updateContent(/** @type {string} **/ text) {
        if (text) {
    
            var resxValues = [];
    
            let json;
            try {
                json = JSON.parse(text);
            }
            catch
            {
                console.log("error parsing json");
                return;
            }
    
            for (const node in json || []) {
                if (node) {
                    let res = json[node];
                    // eslint-disable-next-line @typescript-eslint/naming-convention
                    var item = { Key: node, "Value": res.value || '', "Comment": res.comment || '' };
                    resxValues.push(item);
                }
                else {
                    console.log('node is undefined or null');
                }
            }
    
            table.rowsData = resxValues;
        }
        else {
            console.log("text is null");
            return;
        }
    }
    

    And the vscode-data-grid generates the rows, sticky header, handles the scrolling, theming, responding to environment, etc. for me!

    Grid view

    Now I want to edit…

    Editing in the vscode-data-grid

    The default data-grid does NOT provide editing capabilities unfortunately and I really didn’t want to have to invent something here and end up not getting the value from all the Webview UI Toolkit. Luckily some in the universe also tackling the same problem.  Thankfully Liam Barry was at the same time trying to solve the same problem and helped contribute what I needed.  It works and provides a simple editing experience:

    Editing a row

    Now that I can edit can I delete?

    Deleting items in the grid

    Maybe you made an error and you want to delete.  I decided to expose a command that can be invoked from the command palette but also from a context menu. I specifically chose not to put an “X” or delete button per-row…it didn’t feel like the right UX.  Once I created the command (which basically gets the element and then the _rowData from the vscode-data-grid element (yay, that was awesome the context is set for me!!).  Then I just remove it from the items array and update the doc.  The code is okay, but the experience is simple exposing as a right-click context menu:

    Deleting an item

    This is exposed by enabling the command on the webview context menu via package.json – notice on line 2 is where it is exposed on the context menu and the conditions of which it is exposed (a specific config value and ensuring that my editor is the active one):

    "menus": {
      "webview/context": [
        {
          "command": "resx-editor.deleteResource",
          "when": "config.resx-editor.experimentalDelete == true && activeCustomEditorId == 'resx-editor.editor'"
        }
      ]
    ...
    }
    

    Deleting done, now add a new one!

    Adding a new item

    Obviously you want to add one! So I want to capture input…but don’t want to do a ‘form’ as that doesn’t feel like the VS Code way. I chose to use a multi-input method using the command area to capture the flow. This can be invoked from the button you see but also from the command palette command itself.

    Add new resource

    Simple enough, it captures the inputs and adds a new item to the data array and the document is updated again.

    Using the default editor

    While custom editors are great, there may be times you want to use the default editor. This can be done by doing “open with” on the file from Explorer view, but I wanted to provide a quicker method from my custom editor. I added a command that re-opens the active document in the text editor:

    let openInTextEditorCommand = vscode.commands.registerCommand(AppConstants.openInTextEditorCommand, () => {
      vscode.commands.executeCommand('workbench.action.reopenTextEditor', document?.uri);
    });
    

    and expose that command in the editor title context menu (package.json entry):

    "editor/title": [
    {
      "command": "resx-editor.openInTextEditor",
      "when": "activeCustomEditorId == 'resx-editor.editor' && activeEditorIsNotPreview == false",
      "group": "navigation@1"
    }
    ...
    ]
    

    Here’s the experience:

    Toggle resx raw view

    Helpful way to toggle back to the ‘raw’ view.

    Using the custom editor as a previewer

    But what if you are in the raw view and want to see the formatted one? This may be common for standard formats where users do NOT have your editor set as default. You can expose a preview mode for yours and similarly, expose a button on the editor to preview it. This is what I’ve done here in package.json:

    "editor/title": [
    ...
    {
      "command": "resx-editor.openPreview",
      "when": "(resourceExtname == '.resx' || resourceExtname == '.resw') && activeCustomEditorId != 'resx-editor.editor'",
      "group": "navigation@1"
    }
    ...
    ]
    

    And the command that is used to open a document in my specific editor:

    let openInResxEditor = vscode.commands.registerCommand(AppConstants.openInResxEditorCommand, () => {
    
        const editor = vscode.window.activeTextEditor;
    
        vscode.commands.executeCommand('vscode.openWith',
            editor?.document?.uri,
            AppConstants.viewTypeId,
            {
                preview: false,
                viewColumn: vscode.ViewColumn.Active
            });
    });
    

    Now I’ve got a different ways to see the raw view, preview, or default structured custom view. 

    Preview mode

    Nice!

    Check out the codez

    As I mentioned earlier this is hardly an original idea, but I liked learning, using a standard UX and trying to make sure it felt like it fit within the VS Code experience.  So go ahead and give it an install and play around. It is not perfect and comes with the ‘works on my machine’ guarantee.

    Marketplace listing

    The code is out there and linked in the Marketplace listing for you.

    | Comments

    Whenever I work on something new, I like to make sure I try to better understand the tech and the ecosystem around it.  With the launch of the C# Dev Kit, I had to dive deeper into understanding some things about how VS Code extensions work, and get dirtier with TypeScript/JavaScript more than usual that my ‘day job’ required.  As a part of how I learn, I build.  So I went and built some new extensions.  Nearly all of my experiments are public on my repos, and all come with disclaimers that they usually are for completely selfish reasons (meaning they may not help anyone else but me – or just learning) or may not even be original ideas really.  I only say that because you may know a lot of this already.

    As a part of this journey I’ve loved the VS Code extensibility model and documentation.  It really is great for 99% of the use cases (the remaining 1% being esoteric uses of WebView, some of the VS Code WebView UI Toolkit, etc).  And one thing I’ve come to realize is the subtleties of making your VS Code extension a lot more helpful to your consumers in the information, with very little effort – just some simple entries in package.json in fact.  Here were some that I wasn’t totally aware of (they are in the docs) mostly because my `yo code` starting point doesn’t emit them.  Nearly all of these exist to help people understand your extension, discover it, or interact with YOU better.  And they are simple!

    The Manifest

    First, make sure you understand the extension manifest is not just a package.json for node packages. It represents metadata for your Marketplace and some interaction with VS Code itself!

    VS Code Extension Manifest

    It’s just some snippet of text, but powers a few experiences…here are some I’ve noticed provide some added Marketplace and product value.

    Repository

    Sounds simple, but can be helpful if your extension is Open Source.  The repository just surfaces a specific link in your listing directly to your repository.

    "repository": {
      "type": "git",
      "url": "https://github.com/timheuer/resx-editor"
    }
    

    Simple enough, you specify a type and to the root of your repo.

    Bugs

    You want people to log issues right?  This attribute powers the ‘Report Issue’ capability within VS Code itself:

    VS Code Issue Reporter

    To add this you simply put a URL to the issues collector of your project.  Could be anything really, but if it is a GitHub repo then your users will be able to directly log an issue from within VS Code. If it is not, then the URL of your issues collector (e.g., Jira, Azure DevOps) will be displayed here in link format.

    "bugs": {
      "url": "https://github.com/timheuer/resx-editor/issues"
    }
    

    This is super helpful for your users and I think a requirement!

    Q&A

    By default if you publish to the Visual Studio Marketplace, then you get an Q&A tab for your extension. People can come here and start asking questions. I personally think the experience is not great here right now, as the publisher is the sole respondent, the conversation isn’t threaded, etc. But you can change that.

    "qna": "https://github.com/timheuer/resx-editor/issues"
    

    By adding this, the Q&A link in the marketplace will now direct people to your specific link here rather than bifurcate your Q&A discussions in marketplace and your chosen place. This can be GitHub Issues, GitHub Discussions, some other forum software, whatever. It provides a good entry point so that you don’t have to monitor yet another Q&A portion for your product.

    Keywords

    Yes you have categories (which are specific words that the Marketplace knows about), but you can also have keywords (up to 5). This is helpful when you basically want to add some searchable context/content that might not be in your title or brief description.

    "keywords": [
      "resx",
      "resw",
      "resource",
      "editor",
      "viewer"
    ],
    

    You can only have 5 so tune them well, but don’t leave these out.  They also display in the marketplace listing.

    Badges

    Who doesn’t love a good badge to show build status or versioning! One small delighter for the nerds among us is the Marketplace/manifest support this URL format from a set of approved badge providers.  Adding this in your manifest:

    "badges": [
      {
        "url": "https://img.shields.io/visual-studio-marketplace/v/timheuer.resx-editor?label=VS%20Code%20Marketplace&color=brightgreen&logo=visualstudiocode",
        "href": "https://marketplace.visualstudio.com/items?itemName=TimHeuer.resx-editor",
        "description": "Current Version"
      },
      {
        "url": "https://github.com/timheuer/resx-editor/actions/workflows/build.yaml/badge.svg",
        "href": "https://github.com/timheuer/resx-editor/actions/workflows/build.yaml",
        "description": "Build Status"
      }
    ]
    

    now shows these by default in your Marketplace listing:

    VS Code Marketplace listing with badges

    Maybe a bit ‘extra’ as my daughter would say, but I think it adds a nice touch.

    Snippets

    If you are a code provider or a custom editor you may want to add some snippets.  Your extension can directly contribute them with your other functionality.

    "snippets": [
      {
        "language": "xml",
        "path": "./snippet/resx.json"
      }
    ]
    

    Then when your extension is installed these are just a part of it and you don’t need to provide a ‘snippet only’ pack of sorts.

    Menus

    If you are doing custom things, you likely already know about contributing menus and commands.  But did you know that commands appear in the command palette by default? Perhaps you don’t want that as your command is context specific: only when a certain file type is open, a specific editor is in view, etc. So you’ll want to hide them by default in the command pallette using the ‘when’ clause like in lines 5 and 11 here. I want to never show one in the command palette (when:false) and the other only certain conditions when a specific view is open.

    "menus": {
      "webview/context": [
        {
          "command": "resx-editor.deleteResource",
          "when": "config.resx-editor.experimentalDelete == true && webviewId == 'resx-editor.editor'"
        }
      ],
      "commandPalette": [
        {
          "command": "resx-editor.deleteResource",
          "when": "false"
        }
      ]
    }
    

    This enables the commands to be surfaced where you want like custom views, context menus, etc. without them showing as an ‘anytime’ available command.

    Summary

    There is a lot more you can do and of course the most important thing is providing a useful extension (heck, even if only to you). But these are some really simple and subtle changes I noticed in my learning that I think more extension authors should take advantage of!  Hope this helps!