Writing Snippets and Live Templates: Your Own C# and Angular Shortcuts in VS Code, Visual Studio and Rider
A snippet is the right tool for the small skeletons the language's syntax makes you type every time, not for repeated logic. We show VS Code snippets for Bookstore's xUnit v3 test, minimal API endpoint group, EF Core configuration and Angular 22 component, a transform that derives the namespace from the file path, the Visual Studio .snippet and Rider live template equivalents, and the .gitignore line that silently ignores the snippet file.
Version note: The snippet files in this post and the code they produce were built and run with .NET 10 (SDK 10.0.404), xUnit v3 (xunit.v3.mtp-v2 4.0.3), Entity Framework Core 10.0.15, Angular 22.2 and Node.js 24. The IDE-side behaviour follows the VS Code, Visual Studio 2026 and Rider 2026.3 documentation.
Guess First
In Bookstore, the online bookstore used as the shared example across this site's series, Emre opens a new test file, types bsfact, presses Tab and gets the team's shared test skeleton. The snippet lives in the repo, in .vscode/bookstore.code-snippets. Ayşe clones the same repo, opens the same folder in VS Code, types bsfact and nothing shows up in the list.
Emre says he committed the file, and git log shows the commit. Nothing is wrong with Ayşe's settings. Where did the snippet go? Hint: the culprit is one of the commands that created the repo on day one. The answer is in the "Sharing with the Team" section.
When Is a Snippet the Right Tool?
A snippet is the editor turning a short word into a prewritten block. It is most useful for the small skeletons you have to type every time because of the language's or framework's syntax: a test method, a minimal API endpoint group, an EF Core entity configuration, an Angular component. The structure is always the same and only the names change. You can't remove that structure with an abstraction, because what repeats is syntax, not logic.
It is the wrong tool when you copy the same logic (validation, mapping, error handling) into ten files with a snippet. Code spread across ten copies spreads its bugs to ten places too. A practical test: if you later have to change the generated code the same way in every copy, it shouldn't be a snippet but a helper method, a base class or an extension method.
Before writing your own snippet, look at the built-in ones. Visual Studio ships more than thirty C# snippets such as prop, propfull, ctor, cw, foreach and try; ctor gets the name of the enclosing class through the ClassName() function. Rider has live templates with the same names. In VS Code, the C# extension's snippets/csharp.json contains 42 snippets; its ctor uses the file name ($TM_FILENAME_BASE) rather than the class name, and there is even a fact snippet. The Angular framework doesn't ship snippets; shortcuts like a-component come from John Papa's Angular Snippets extension.
If the built-in fact does the job, don't write your own. Our team wrote one because it wanted two things: the Method_Scenario_ExpectedResult naming rule and a one-key choice between [Fact] and [Theory].
VS Code: The .code-snippets File
VS Code keeps snippets in JSON files. There are three places: a per-language user file (for example csharp.json), a global .code-snippets file valid in every language, and .code-snippets files in the .vscode folder at the project root. The third one is right for a team, because it comes with the repo. The first snippet in Bookstore's file:
{
"xUnit v3 fact": {
"scope": "csharp",
"prefix": "bsfact",
"description": "Bookstore: xUnit v3 test named Method_Scenario_ExpectedResult",
"body": [
"[${1|Fact,Theory|}]",
"public ${2|void,async Task|} ${3:Method}_${4:Scenario}_${5:ExpectedResult}()",
"{",
" $0",
"}"
]
}
}
prefix is the trigger word, body the text inserted line by line, description the text shown in IntelliSense. scope ties the snippet to languages; the value is VS Code's language identifier (csharp, typescript, html). According to the documentation, a global snippet without scope is offered in every language; you don't want a bsfact suggestion while writing YAML.
The markers in the body: $1 and $2 are tab stops visited in order with Tab, and $0 is the final cursor position. ${3:Method} is a placeholder with default text and ${1|Fact,Theory|} is a choice picked from a list. If the same number appears twice, both places change together. Snippets show up in the IntelliSense list mixed with other suggestions; if you want them to expand with Tab alone, you need the "editor.tabCompletion": "on" setting.
A test written with bsfact and its run:
public sealed class InMemoryBookCatalogTests
{
[Fact]
public void FindByIsbn_UnknownIsbn_ReturnsNull()
{
InMemoryBookCatalog catalog = new();
Book? book = catalog.FindByIsbn("9780000000000");
Assert.Null(book);
}
}
$ dotnet test
Test run summary: Passed!
total: 2
failed: 0
succeeded: 2
skipped: 0
duration: 242ms
For snippets that fill a whole file there is "isFileTemplate": true: running "Snippets: Fill File with Snippet" from the Command Palette in an empty file lists them. With the include and exclude globs you can limit a snippet to certain files; when they conflict, exclude wins.
This section is based on the official documentation.
Variables and Transforms: Deriving the Namespace from the File Path
The real time saver is variables. TM_FILENAME_BASE is the file name without extension, RELATIVE_FILEPATH the file path relative to the workspace root, and there are values such as CURRENT_YEAR, UUID and CLIPBOARD. Any variable can take a transform in the form ${VAR/regex/format/options}. Bookstore's endpoint and configuration snippets derive the namespace from the file path:
"namespace ${RELATIVE_FILEPATH/^\\w+\\W|\\W[\\w.]+$|(\\W)/${1:+.}/g};",
The regex has three alternatives: the leading src/ or tests/ part, the trailing /BookConfiguration.cs part and every separator in between. The first two become empty and a separator becomes a dot; ${1:+.} means "write a dot if group one matched". I write the separator as \W instead of / because of Windows, where RELATIVE_FILEPATH comes with backslashes. There is a second reason: the parts of a transform are separated by /, so a / inside the regex has to be escaped, and \W avoids that problem entirely.
VS Code runs transforms with the JavaScript regex engine, so you can try the regex in Node before putting it into the snippet:
// VS Code runs transforms with JavaScript regular expressions,
// so the same regex can be tried in Node before it goes into the snippet.
const ns = (path) =>
path.replace(/^\w+\W|\W[\w.]+$|(\W)/g, (_match, sep) => (sep ? '.' : ''));
for (const path of [
'src/Bookstore.Api/BookEndpoints.cs',
'src\\Bookstore.Catalog\\Persistence\\BookConfiguration.cs',
'tests/Bookstore.Catalog.Tests/BookConfigurationTests.cs',
]) {
console.log(`${path} -> ${ns(path)}`);
}
console.log('BookConfiguration ->', 'BookConfiguration'.replace(/(.*)Configuration$/, '$1'));
$ node tools/snippets/check-transforms.mjs
src/Bookstore.Api/BookEndpoints.cs -> Bookstore.Api
src\Bookstore.Catalog\Persistence\BookConfiguration.cs -> Bookstore.Catalog.Persistence
tests/Bookstore.Catalog.Tests/BookConfigurationTests.cs -> Bookstore.Catalog.Tests
BookConfiguration -> Book
The format part also has case conversions: /upcase, /downcase, /capitalize, /camelcase, /pascalcase, /kebabcase, /snakecase. The Angular snippet derives the class name BookCard from the file name book-card with ${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}.
Bookstore's Snippets: Endpoint Group, EF Core and Angular
The file has four snippets: bsfact, bsendpoints, bsconfig and bscomponent. bsconfig takes the entity name from a file whose name ends with Configuration. The snippet's body:
"body": [
"using Microsoft.EntityFrameworkCore;",
"using Microsoft.EntityFrameworkCore.Metadata.Builders;",
"",
"namespace ${RELATIVE_FILEPATH/^\\w+\\W|\\W[\\w.]+$|(\\W)/${1:+.}/g};",
"",
"internal sealed class ${TM_FILENAME_BASE} : IEntityTypeConfiguration<${1:${TM_FILENAME_BASE/(.*)Configuration$/$1/}}>",
"{",
" public void Configure(EntityTypeBuilder<${1}> builder)",
" {",
" builder.ToTable(\"${2:${1}s}\");",
" builder.HasKey(x => x.${3:Id});",
" $0",
" }",
"}"
]
Invoked in src/Bookstore.Catalog/Persistence/BookConfiguration.cs, with Isbn typed instead of Id and two lines added, it gives this file:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Bookstore.Catalog.Persistence;
internal sealed class BookConfiguration : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("Books");
builder.HasKey(x => x.Isbn);
builder.Property(x => x.Isbn).HasMaxLength(13);
builder.Property(x => x.Price).HasPrecision(10, 2);
}
}
Before writing the Angular snippet I looked at what the Angular 22 CLI generates, because a snippet that produces different code from the CLI gives the team two styles. ng generate component book-card creates a file named book-card.ts with a class named BookCard; there is no .component suffix, no standalone: true and no changeDetection line. The comment on ChangeDetectionStrategy in Angular 22 says OnPush is now the default. So the snippet adds only what is needed; the body of bscomponent:
"body": [
"import { Component, input } from '@angular/core';",
"import { ${2:Book} } from '${3:../book}';",
"",
"@Component({",
" selector: 'app-${TM_FILENAME_BASE}',",
" templateUrl: './${TM_FILENAME_BASE}.html',",
" styleUrl: './${TM_FILENAME_BASE}.css',",
"})",
"export class ${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/} {",
" readonly ${1:book} = input.required<${2}>();",
" $0",
"}"
]
The files the snippets produced live in the repo as real code and get compiled, so if a snippet breaks, the build is the first to notice:
$ dotnet build
0 Warning(s)
0 Error(s)
$ npx ng test --watch=false
Test Files 2 passed (2)
Tests 2 passed (2)
Visual Studio: The .snippet XML
Visual Studio expresses the same idea in XML. The equivalent of bsfact:
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>xUnit v3 fact</Title>
<Shortcut>bsfact</Shortcut>
<Description>Bookstore: xUnit v3 test named Method_Scenario_ExpectedResult</Description>
<Author>Bookstore team</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Imports>
<Import>
<Namespace>Xunit</Namespace>
</Import>
</Imports>
<Declarations>
<Literal>
<ID>method</ID>
<ToolTip>Method under test</ToolTip>
<Default>Method</Default>
</Literal>
<Literal>
<ID>scenario</ID>
<ToolTip>Input or state</ToolTip>
<Default>Scenario</Default>
</Literal>
<Literal>
<ID>expected</ID>
<ToolTip>Expected result</ToolTip>
<Default>ExpectedResult</Default>
</Literal>
</Declarations>
<Code Language="CSharp" Kind="method decl">
<![CDATA[[Fact]
public void $method$_$scenario$_$expected$()
{
$end$
}]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
A Literal is an editable field: it is defined with an ID, used in the code as $method$, and its Default is required. $end$ is the final cursor position and $selected$ the selected text; each can be used once. Kind="method decl" makes the snippet show up only inside a class, where a method can be declared. Imports adds using Xunit; to the file if it's missing. One restriction is why the VS Code name is also bsfact: Shortcut accepts only letters, digits and underscores, so bs-fact won't do.
The difference from VS Code is large: C# snippets have only three functions (ClassName(), SimpleTypeName(), GenerateSwitchCases()); there is no value from the file name and no regex transform. To install, open Code Snippets Manager from the Tools menu (Ctrl+K, Ctrl+B); Import copies the file under %USERPROFILE%\Documents\Visual Studio 18\Code Snippets\Visual C#\My Code Snippets. For a team, the Add button in the same window is better: it adds the repo's tools/snippets/visualstudio folder as a snippet folder, so a change that arrives with git pull shows up directly. In C#, typing the shortcut and pressing Tab twice expands the snippet; Ctrl+K, X lets you pick from a list. For wider distribution, the documentation describes putting snippets into a VSIX extension.
This section is based on the official documentation.
Rider: Live Templates and Macros
Rider's equivalent is the live template. You create it in Settings (Ctrl+Alt+S) on the Editor | Live Templates page; variables are written between dollar signs like $METHOD$, $END$ is the final cursor position and $SELECTION$ the selected text. The real power is macros: Rider computes the value of a variable. Useful ones from the documented list are typeName() for the containing type's name, getFileNameWithoutExtension(), getCurrentNamespace(), getDefaultNamespace(), suggestVariableName() and complete(). There is no need to derive the namespace with a regex; Rider knows the project.
Rider keeps settings in .DotSettings files as XML. Settings are layered: the machine-wide "This computer" layer, the "Solution team-shared" layer next to the solution that gets committed (<SolutionName>.sln.DotSettings), and the personal .DotSettings.user that doesn't. If you pick the team-shared layer with Save To when saving a live template, it is written next to the solution file. You can also export to and import from a file in the Settings Layers window (Manage Layers). A part of Bookstore's tools/snippets/rider/bookstore.DotSettings:
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7D2E4C1A9B3F4E8A8C6D5B4A3F2E1D0C/Shortcut/@EntryValue">bsfact</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7D2E4C1A9B3F4E8A8C6D5B4A3F2E1D0C/Text/@EntryValue">[Fact]
public void $METHOD$_$SCENARIO$_$EXPECTED$()
{
$END$
}</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7D2E4C1A9B3F4E8A8C6D5B4A3F2E1D0C/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/Type/@EntryValue">InCSharpTypeMember</s:String>
You don't have to write this file by hand; creating the template in Rider and saving it to the team-shared layer produces the same lines. I took the key structure from the .sln.DotSettings file of a real open-source project. The template expands with Tab, you move between variables with Tab and Shift+Tab, and Ctrl+Alt+J surrounds the selected code with a template.
This section is based on the official documentation.
The three IDEs side by side:
| VS Code | Visual Studio | Rider | |
|---|---|---|---|
| File | .vscode/*.code-snippets (JSON) | .snippet (XML) | .DotSettings (XML) |
| Variable | $1, ${1:Method} | $method$ + Literal | $METHOD$ |
| Final cursor | $0 | $end$ | $END$ |
| Selected text | TM_SELECTED_TEXT | $selected$ | $SELECTION$ |
| Computed value | variable + regex transform | 3 functions | macros |
| Team sharing | .vscode in the repo | Code Snippets Manager, Add | team-shared layer |
Sharing with the Team
The answer to Guess First is .gitignore. Both the file from dotnet new gitignore and the one the Angular CLI generates contain a .vscode/* line and make exceptions only for settings.json, tasks.json, launch.json and extensions.json. Emre's commit contains the other changes he made that day, but the snippet isn't in its file list: git add . skipped the ignored file without any warning. git check-ignore shows which line swallowed the file:
$ git check-ignore -v .vscode/bookstore.code-snippets
.gitignore:388:.vscode/* .vscode/bookstore.code-snippets
The fix is one line: add !.vscode/*.code-snippets below the !.vscode/extensions.json line. Then the same command:
$ git check-ignore -v .vscode/bookstore.code-snippets
.gitignore:393:!.vscode/*.code-snippets .vscode/bookstore.code-snippets
A match on a line starting with ! means the file is no longer ignored. On the Rider side, the same .gitignore contains a *.DotSettings.user line; the personal layer stays out and the team-shared file can be committed. That's the right behaviour, but take care not to save a personal setting into the team-shared file.
Then there are tools outside the IDE. Text expansion tools such as espanso work in every application: a fixed line added to commit messages, an ISBN you type often, the test e-mail ada@example.com. Their settings live in a YAML file such as match/base.yml as trigger and replace pairs. Don't put code templates into these tools; the scope, tab stops and formatting the IDE knows aren't there. They are personal tools and get versioned with your dotfiles (the fourth post in this series).
Production Notes
A snippet file is code too; it gets reviewed in pull requests and it can break. Bookstore has a small check script for this that CI can run on every build: whether the JSON can be read, whether every snippet has prefix, body and scope, whether every Literal in the Visual Studio file has a Default and is used in the code, and whether the Rider file is valid XML:
$ python3 -I tools/snippets/check-snippets.py
VS Code: 4 snippets
Visual Studio: 'xUnit v3 fact' (CSharp, method decl)
Rider: 16 keys, shortcuts ['bsfact']
The second check is that the code the snippets produce compiles. Bookstore's Directory.Build.props sets AnalysisLevel to latest-recommended and treats warnings as errors. That's why the first test produced by bsfact broke the build:
tests/Bookstore.Catalog.Tests/BookConfigurationTests.cs(10,17): error CA1707: Remove the underscores from member name Bookstore.Catalog.Tests.BookConfigurationTests.Configure_BookEntity_UsesIsbnAsKeyAndBooksTable() (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1707) [tests/Bookstore.Catalog.Tests/Bookstore.Catalog.Tests.csproj]
CA1707 forbids underscores in public member names, while underscores in test names are a deliberate rule. The fix was to turn the warning off for test projects only, in tests/Directory.Build.props:
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<!-- Test names use Method_Scenario_ExpectedResult. -->
<NoWarn>$(NoWarn);CA1707</NoWarn>
</PropertyGroup>
</Project>
The snippet and the analyzer rule are two sides of the same team decision; when one changes, update the other. The rules themselves are in the Coding Conventions and Team Standards series.
Common Pitfalls
The first is the stale snippet. A component snippet written a few years ago may still produce this today:
@Component({
selector: 'app-book-card',
standalone: true,
templateUrl: './book-card.component.html',
})
export class BookCardComponent {
@Input() book!: Book;
}
It compiles, but it differs from what Angular 22 generates in three places: the redundant standalone: true, file names with the .component suffix, and the @Input() decorator instead of a signal input. Put snippets on the checklist for every major framework upgrade; the easiest way is to place the snippet's output next to the ng generate output.
The second is embedding values in a snippet. If a connection string snippet starts with a value like Password=example-password-change-me, one day that value gets replaced with a real password and committed. Leave the name of an environment variable such as ${1:ConnectionStrings__Bookstore} in the snippet instead of a value; the value should come from user-secrets or an environment variable.
The third is a prefix clash. If you write a snippet called prop or fact, it shows up in the same list as the built-in one and it becomes unclear which one gets picked. Give team snippets a common prefix; in Bookstore it's bs. The fourth is forgetting scope: a global snippet without scope is offered in every language. The fifth, as I said at the start, is using a snippet instead of an abstraction.
A Small Experiment
★☆☆ Copy the bsfact snippet into your editor and write a second snippet that also adds an [InlineData()] line when [Theory] is chosen. Check with check-snippets.py that the JSON is valid.
★★☆ Add a second tab stop for group.MapPost to the bsendpoints snippet and derive the route name (books) from the entity name (Book) with a transform. Try the transform in check-transforms.mjs first.
★★★ Check with git check-ignore -v whether the .gitignore in your own project has a .vscode/* line. Then create a live template for bsfact in Rider, save it to the team-shared layer and compare the resulting .DotSettings file with the one in this post.
A snippet solves the boilerplate inside a single file. In the next post we go one level up: generating a new Bookstore service with global.json, Directory.Build.props, analyzer settings, a test project and a Dockerfile in one command with a dotnet new template, plus a short look at schematics on the Angular side.