Scaffolder .NET Actions logo

Backstage Scaffolder .NET Actions Plugin

Created by Alef Carlos

Scaffolder .NET Actions adds dotnet tooling to your Backstage scaffolder. It exposes template actions that wrap the dotnet CLI so your templates can spin up new .NET projects during a scaffolding run. You keep the flow inside Backstage. No context switching.

At its core it provides a dotnet new action. A template step can call dotnet new to create a web API or another project type from the standard templates. Recent community releases ship this through the Backstage community plugins scope.

This is useful if you run .NET in production and want paved roads in your portal. Platform teams can give engineers an easy way to create a service with the right structure, naming, and defaults. A single template can create the repo, scaffold the .NET project, publish to your git provider, then register in the catalog. You can standardize the first mile for APIs, web apps, or Aspire based projects while keeping the template readable and versioned in git.

Installation Instructions

These instructions apply to self-hosted Backstage only.

Install the package

Run this in the repo root

Copy
yarn workspace backend add <the plugin package name from the registry>

If the plugin ships any frontend package run this too

Copy
yarn workspace app add <the frontend package name if present>

Replace the placeholders with the exact package names from the plugin page

Wire it into the new backend system

If your backend uses the new backend system with createBackend do this

Add the module to packages backend src index ts

Copy
// packages/backend/src/index.ts
import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();

// The plugin module usually exports a function you can add to the backend
// Replace the import path and the exported symbol with the ones from the plugin
backend.add(
  import('<the plugin package name>').then(m => 
    // common export names look like scaffolderModuleDotnet or dotnetModule
    // check the plugin for the exact function
    m.scaffolderModuleDotnet()
  ),
);

backend.start();

If the module needs config add that under app config yaml as the plugin docs describe

Wire it into the old backend system

If your backend still uses the old createRouter pattern hook the actions into the scaffolder backend

Add imports in packages backend src plugins scaffolder ts

Copy
// packages/backend/src/plugins/scaffolder.ts
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import { createRouter } from '@backstage/plugin-scaffolder-backend';

// Replace the import path with the plugin package
// Replace the imported functions with the ones the plugin exports
import {
  createDotnetNewAction,
  createDotnetRestoreAction,
  createDotnetBuildAction,
  createDotnetTestAction,
  createDotnetPublishAction,
  createNugetPushAction,
} from '<the plugin package name>';

export default async function createPlugin(env: PluginEnvironment): Promise<Router> {
  const actions = [
    createDotnetNewAction(),
    createDotnetRestoreAction(),
    createDotnetBuildAction(),
    createDotnetTestAction(),
    createDotnetPublishAction(),
    createNugetPushAction(),
  ];

  return await createRouter({
    logger: env.logger,
    config: env.config,
    database: env.database,
    reader: env.reader,
    catalogClient: env.catalogClient,
    // register the custom actions
    actions,
  });
}

Make sure packages backend src index ts mounts the scaffolder router

Copy
// packages/backend/src/index.ts
import scaffolder from './plugins/scaffolder';

// inside main bootstrap where apiRouter is created
const scaffolderRouter = await scaffolder(env);
apiRouter.use('/scaffolder', scaffolderRouter);

If the plugin exposes any other backend helpers import and wire them as the docs show

Make the actions visible in your Scaffolder UI

This plugin is backend only. You do not add any new visual components from it. Your users will interact with the actions through templates

Ensure the Scaffolder page is routed in the frontend

Copy
// packages/app/src/App.tsx
import React from 'react';
import { Route } from 'react-router';
import { FlatRoutes } from '@backstage/core-app-api';
import { ScaffolderPage } from '@backstage/plugin-scaffolder';

// inside your app routes
export const AppRoutes = () => (
  <FlatRoutes>
    <Route path="/create" element={<ScaffolderPage />} />
    {/* other routes */}
  </FlatRoutes>
);

Add a sidebar item so users can find it

Copy
// packages/app/src/components/Root/Root.tsx
import React from 'react';
import {
  Sidebar,
  SidebarItem,
  SidebarDivider,
} from '@backstage/core-components';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';

export const Root = ({ children }: { children?: React.ReactNode }) => (
  <>
    <Sidebar>
      {/* other items */}
      <SidebarDivider />
      <SidebarItem to="/create" text="Create" icon={CreateComponentIcon} />
    </Sidebar>
    {children}
  </>
);

Add a template that uses the dotnet actions

Create a template file in your catalog. For example catalog templates dotnet starter template yaml

Below is an example. Replace action ids and input keys with the ones documented by the plugin

Copy
# catalog/templates/dotnet-starter/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: dotnet-starter
  title: New dotnet service
  description: Create a dotnet service from a template
spec:
  owner: team-a
  type: service
  parameters:
    - title: Service info
      required:
        - name
      properties:
        name:
          type: string
          title: Service name
        solutionName:
          type: string
          title: Solution name
        templateShortName:
          type: string
          title: dotnet template short name
          default: webapi
  steps:
    - id: dotnet-new
      name: Create project
      action: dotnet:new
      input:
        workingDirectory: .
        template: '${{ parameters.templateShortName }}'
        name: '${{ parameters.name }}'
        output: '${{ parameters.solutionName }}'
    - id: dotnet-restore
      name: Restore
      action: dotnet:restore
      input:
        workingDirectory: '${{ steps.dotnet-new.output.path }}'
    - id: dotnet-build
      name: Build
      action: dotnet:build
      input:
        workingDirectory: '${{ steps.dotnet-new.output.path }}'
        configuration: Release
    - id: dotnet-test
      name: Test
      action: dotnet:test
      input:
        workingDirectory: '${{ steps.dotnet-new.output.path }}'
    - id: dotnet-publish
      name: Publish
      action: dotnet:publish
      input:
        workingDirectory: '${{ steps.dotnet-new.output.path }}'
        configuration: Release
        output: dist
    # If you plan to push to a package feed
    # - id: nuget-push
    #   name: Push package
    #   action: nuget:push
    #   input:
    #     packagePath: 'dist/*.nupkg'
    #     source: 'https://your-nuget-source'
    #     apiKey: '${{ secrets.nugetApiKey }}'
  output:
    links:
      - title: Open the new project
        url: '${{ steps.dotnet-new.output.path }}'

Register the template location in your app config if you are not already loading templates from that folder

Copy
# app-config.yaml
catalog:
  locations:
    - type: file
      target: catalog/templates/**/*.yaml
      rules:
        - allow: [Template]

Notes on action ids and exports

Action ids like dotnet new or dotnet restore vary by plugin. The code above uses common ids as placeholders. Use the exact ids listed by the plugin

For the new backend system the module function name can differ. Common names look like scaffolderModuleDotnet. Use the exact export from the plugin

For the old backend system the action factory function names can differ. Use the exact names from the plugin and add them to the actions array as shown above

Build and run

Install workspace deps

Copy
yarn install

Start backend and app in separate terminals

Copy
yarn workspace backend start
Copy
yarn workspace app start

Changelog

The Scaffolder .NET Actions plugin has not seen any significant changes since a year ago.

Set up Backstage in minutes with Roadie