Skip to main content
net8.0

A configurable performance test data seeder for Umbraco CMS v13 for performance and load testing. Automatically seeds languages, dictionary items, data types, document types, media, content, and users.

Published
May 19, 2026
5mo ago
Total Downloads
444
444

Install

dotnet add package Umbraco.Cms.TestDataSeeder --version 13.0.0-beta.1
ProductVersions
.NET
net8.0

README

Umbraco.Cms.TestDataSeeder

NuGet License: MIT

A configurable performance / load-testing data seeder for Umbraco CMS. Automatically creates languages, dictionary items, data types, document types (including BlockGrid and BlockList compositions), media, content, users, and members on application startup.

⚠️ Not for production use. This package creates large volumes of content, media, users and members. It is intended for performance and load testing only. Install it in dedicated test environments.

Compatibility

The package major aligns with the supported Umbraco CMS major, matching the rest of the Umbraco.Cms.* package family.

Package version Umbraco .NET
13.x Umbraco 13 .NET 8

Support for Umbraco 17 ships on a separate release train (17.x).

Installation

dotnet add package Umbraco.Cms.TestDataSeeder --version 13.*

The package auto-registers via an IComposer, so no startup wiring is required in the host project. By default the seeder is disabled; enable it explicitly in appsettings.json (see below).

Configuration

Configuration lives under the Umbraco.Cms.TestDataSeeder root in appsettings.json, split into two sub-sections:

  • Umbraco.Cms.TestDataSeeder:Options β€” runtime behaviour (enable/disable, preset, batching, publishing mode, domain suffix, etc.)
  • Umbraco.Cms.TestDataSeeder:Configuration β€” target counts per data type (languages, content, media, document types, …)

Minimal enable

{
  "Umbraco.Cms.TestDataSeeder": {
    "Options": {
      "Enabled": true,
      "Preset": "Medium"
    }
  }
}

With Preset set, the counts under Umbraco.Cms.TestDataSeeder:Configuration are overridden by the preset values. Use "Custom" (default) to drive counts from the Configuration section instead.

Presets

Preset Approximate total items Use case
Small ~180 Quick smoke tests
Medium ~1,400 Moderate local testing
Large ~16,000 Performance testing
Massive ~74,000 Stress testing
Custom driven by SeederConfiguration Full manual control

Default data quantities (Preset = Custom, no overrides)

Reference numbers for what the seeder produces when nothing is overridden. Each row maps to the property of the same name in Umbraco.Cms.TestDataSeeder:Configuration; the totals are what the corresponding Total* getters on those configs return.

Category Default count Source
Languages 20 Languages.Count
Dictionary items 1,500 Dictionary.TotalItems (10 Γ— 5 Γ— 30)
Data types (base) 120 DataTypes.Total (ListView 30 + MultiNodeTreePicker 40 + RichTextEditor 10 + MediaPicker 10 + Textarea 10 + Dropdown 10 + Numeric 10)
Block List / Block Grid data types 100 DocumentTypes.BlockList (40) + DocumentTypes.BlockGrid (60)
Element types 130 DocumentTypes.ElementTypes.Total (Simple 65 + Medium 39 + Complex 26)
Document types 150 + 6 detail DocumentTypes.TotalDocTypes (Variant 110 + Invariant 40), plus 6 fixed Detail doc types (Simple/Medium/Complex Γ— Variant/Invariant)
Templates 150 DocumentTypes.TotalTemplates
Media items 610 Media.TotalCount (PDF 200 + PNG 200 + JPG 200 + Video 10)
Content nodes 300 Content.TotalTarget
Users 30 Users.Count
Members 30 Members.Count
Frontend pages 3 (member login, member area, contact form) created by MemberSeeder + ContactFormSeeder

Presets scale these counts up or down; see the preset summary table above for approximate totals at each tier.

Runtime options (Umbraco.Cms.TestDataSeeder:Options)

Key Default Purpose
Enabled false Master switch. Must be set to true for seeding to run.
Preset Custom Small / Medium / Large / Massive / Custom.
StopOnError false Abort the whole run on the first seeder failure.
DryRun false Log what would be created without persisting.
FakerSeed null Fixed seed for reproducible data. null uses the current time.
BatchSize 50 Items per database batch.
ParallelDegree 4 Max parallelism for CPU-bound work (e.g. image generation).
PublishMode FirstSection None (draft), All, or FirstSection (publish first root only).
PublishBatchSize 50 Content items published per batch. 0 = publish immediately.
DomainSuffix "localhost" Host used for generated content domains; include port for local dev.
SkipContentDomains false Skip creating content domains entirely.
RebuildCacheAfterSeeding true Rebuild the published content cache after the run.
ProgressIntervalPercent 10 Log progress every N percent. 0 disables progress logs.
EnabledSeeders.* all true Per-seeder toggles (Languages, Dictionary, DataTypes, DocumentTypes, Media, Content, Users, Members, ContactForm).
Prefixes.* Test_ / testElement / … Naming prefixes for generated entities.
CustomCultures null Override the built-in 30-culture pool.

Custom counts (Umbraco.Cms.TestDataSeeder:Configuration)

When Preset = Custom, the Configuration sub-section controls how many of each entity to create.

Top-level sections: Languages, Dictionary, Users, DataTypes, DocumentTypes (with ElementTypes / VariantDocTypes / InvariantDocTypes complexity breakdown plus BlockList / BlockGrid / NestingDepth / MaxBlocksPerEditor), Media (per-type counts for PDF / PNG / JPG / Video), Content (counts, content-complexity distribution, tree shape), and Members (counts, default password, Standard/Premium/VIP distribution).

Full example (appsettings.Test.json)

Combining both sub-sections in one file:

{
  "Umbraco.Cms.TestDataSeeder": {
    "Options": {
      "Enabled": true,
      "Preset": "Custom",
      "FakerSeed": 42,
      "DomainSuffix": "localhost:44340",
      "PublishMode": "FirstSection",
      "DryRun": false
    },
    "Configuration": {
      "Languages":   { "Count": 10 },
      "Users":       { "Count": 10 },
      "Members":     { "Count": 20, "StandardPercent": 50, "PremiumPercent": 30, "VIPPercent": 20 },
      "Content":     { "TotalTarget": 500 },
      "Media":       { "JPG": { "Count": 100, "FolderCount": 5 } }
    }
  }
}

Content hierarchy and document types

ContentSeeder produces a 4-level content tree using the doc types generated by DocumentTypeSeeder:

Section (root)    β†’ uses simple/medium/complex doc types (with Collection)
  └── Category    β†’ uses simple/medium/complex doc types (with Collection)
        └── Page  β†’ uses simple/medium/complex doc types (with Collection)
              └── Detail β†’ uses dedicated Detail doc types (no Collection)

Collection (List View). Section, Category, and Page doc types are configured with the built-in "List View – Content" collection and explicit AllowedContentTypes, so each parent renders its children as a list in the backoffice and editors can create children inline.

Detail doc types are leaf nodes β€” six fixed types (Simple/Medium/Complex Γ— Variant/Invariant) with no collection and no allowed children. Detail nodes inherit the same simple/medium/complex distribution as Pages, controlled by Content.DetailSimplePercent / DetailMediumPercent / DetailComplexPercent (default 60/30/10).

The Section/Category/Page complexity mix is driven by Content.SimplePercent / MediumPercent / ComplexPercent (default 50/30/20). Tree shape is controlled by Content.RootSections, CategoriesPerSection, and PagesPerCategory.

Nested blocks

DocumentTypes.NestingDepth controls how deep blocks can be nested inside one another inside seeded Block List and Block Grid editors. Higher values produce more realistic load-test content because they stress JSON serialization, backoffice rendering, and the Delivery API in the same way deeply nested real-world content does.

NestingDepth = 1 means no nesting (blocks contain only simple/medium/complex element data). Each additional level adds one wrapper layer of NestedBlock_DepthN containers, with the leaf level holding the real element data.

NestingDepth = 2:

BlockList
  └── NestedBlock_Depth1
        └── nestedBlocks (BlockList)
              └── Simple / Medium / Complex elements

NestingDepth = 4:

BlockList
  └── NestedBlock_Depth1
        └── nestedBlocks β†’ NestedBlock_Depth2
                            └── nestedBlocks β†’ NestedBlock_Depth3
                                                └── nestedBlocks β†’ Simple / Medium / Complex elements

Preset defaults: Small = 2, Medium = 4, Large = 6, Massive = 8. The MaxBlocksPerEditor option bounds how many distinct block types each Block List / Block Grid data type advertises (default 30); raising it makes block-picker rendering heavier without changing the tree shape.

How seeding runs

TestDataSeederComposer registers all services and an async notification handler for UmbracoApplicationStartedNotification. When the hosting app starts and Options.Enabled is true, SeederOrchestrator runs every registered ISeeder in execution order. Configuration is validated first; if validation fails and StopOnError is true the run is aborted.

Progress is logged via the standard ILogger pipeline. A summary table is emitted at the end of the run.

Test endpoints

For load-test scenarios the package also exposes HTTP endpoints under the umbraco/api/ prefix.

⚠️ Security model. Every endpoint below is anonymous β€” intentionally so, so load-test runners (k6, JMeter, …) can discover targets and exercise contact-form / member-auth flows without managing credentials. This is safe only in dedicated test environments. Do not expose a host running this package to the public internet. See SECURITY.md.

Route Auth Purpose
GET /umbraco/api/seederstatus/status Anonymous Seeding status, current seeder, elapsed, plus the active Preset and FakerSeed.
GET /umbraco/api/seederstatus/inventory Anonymous Catalog of seeded root-section URLs, sample content URLs (each annotated with complexity and docType so scripts can pick e.g. "complex Detail pages only"), media URLs, member username pattern, and prefixes. Use this for dynamic target discovery in k6.
POST /umbraco/api/contactform/submit Anonymous (JSON) Submit a seeded contact-form entry.
POST /umbraco/api/contactform/form-submit Anonymous + anti-forgery Form-encoded submit (requires @Html.AntiForgeryToken()).
GET /umbraco/api/contactform/stats Anonymous Contact-form submission stats.
POST /umbraco/api/memberlogin/login Anonymous + anti-forgery Log a seeded member in (form POST).
POST /umbraco/api/memberlogin/logout Anonymous + anti-forgery Log out (form POST).
POST /umbraco/api/memberauth/login Anonymous (JSON) Member auth login.
POST /umbraco/api/memberauth/logout Anonymous (JSON) Member auth logout.
GET /umbraco/api/memberauth/me Anonymous (reports state) Returns current member info; reports success=false if no authenticated cookie/session is present.

/status HTTP response codes

GET /umbraco/api/seederstatus/status returns its JSON body alongside an HTTP status code chosen so CI and load-test scripts can wait for a clean signal without parsing the body:

Code When Status field values
200 Seeding finished (success, with errors, or skipped). Completed, CompletedWithErrors, Skipped
202 Seeding hasn't finished yet (still running or waiting to start). Running, NotStarted
503 Seeding failed. Failed

Shell example β€” block a CI step until the seeder reports done or fails:

while :; do
  code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/umbraco/api/seederstatus/status)
  case "$code" in
    200) echo "seeding complete"; break ;;
    503) echo "seeding failed"; exit 1 ;;
    *)   sleep 5 ;;
  esac
done

Resetting between runs

IsAlreadySeeded() checks make the seeder idempotent β€” re-running with the same configuration is a no-op. To get a clean slate (e.g. to switch presets or change FakerSeed), drop the Umbraco database and let unattended install rebuild it:

rm path/to/Umbraco.sqlite.db
dotnet run

For SQL Server hosts, drop and recreate the database. CI pipelines should provision a fresh database per run rather than mutating a shared one.

Member password handling

The seeded member password is intentionally not returned by any HTTP endpoint β€” an unauthenticated status endpoint exposing shared credentials is an unnecessary footgun, even in test environments. Load-test scripts should read the password from an out-of-band source:

# in CI / load-test env
export SEEDER_MEMBER_PASSWORD="Test1234!"

Then in the script:

# locust example
import os
password = os.environ["SEEDER_MEMBER_PASSWORD"]

Keep the value in sync with Umbraco.Cms.TestDataSeeder:Configuration:Members:DefaultPassword in the host's appsettings.json.

Locust example

A minimal Locust script that discovers targets from /inventory, picks a random seeded member, logs in on on_start (so the auth cookie persists across the user's tasks), exercises /me and a contact-form submission, and logs out on on_stop. The member count is derived from memberUsernamePattern (e.g. TestMember_{1..30}), so the script tracks whatever the host is configured for without redeploying.

import os
import random
import re
from locust import HttpUser, between, events, task

MEMBER_PASSWORD = os.environ["SEEDER_MEMBER_PASSWORD"]  # see "Member password handling" above

# Populated once at test start so every user shares the discovered inventory.
INVENTORY = {}


@events.test_start.add_listener
def on_test_start(environment, **_):
    import requests
    base = environment.host.rstrip("/")
    inv = requests.get(f"{base}/umbraco/api/seederstatus/inventory", verify=False).json()
    match = re.search(r"\{1\.\.(\d+)\}", inv["memberUsernamePattern"])
    INVENTORY["prefix"] = inv["prefixes"]["member"]
    INVENTORY["count"] = int(match.group(1)) if match else 0
    INVENTORY["sample_url"] = (inv.get("sampleContentUrls") or [{}])[0].get("url")


class SeederUser(HttpUser):
    wait_time = between(1, 3)

    def on_start(self):
        i = random.randint(1, INVENTORY["count"])
        self.username = f"{INVENTORY['prefix']}{i}"
        self.client.post(
            "/umbraco/api/memberauth/login",
            json={"username": self.username, "password": MEMBER_PASSWORD},
            name="POST /memberauth/login",
        )

    def on_stop(self):
        self.client.post("/umbraco/api/memberauth/logout", name="POST /memberauth/logout")

    @task(3)
    def me(self):
        self.client.get("/umbraco/api/memberauth/me", name="GET /memberauth/me")

    @task(1)
    def submit_contact(self):
        self.client.post(
            "/umbraco/api/contactform/submit",
            json={
                "name": self.username,
                "email": f"{self.username.lower()}@example.com",
                "subject": "Load test",
                "message": "Hello from Locust",
            },
            name="POST /contactform/submit",
        )

Run with:

SEEDER_MEMBER_PASSWORD=Test1234! locust -f locustfile.py --host https://localhost:44340

Then open the Locust web UI (default http://localhost:8089) to configure users and ramp.

Building from source

dotnet restore
dotnet build
dotnet pack -c Release

Targets .NET 8 and Umbraco [13.0.0, 15.0.0).

Contributing

Issues and pull requests are welcome. Please open an issue first for larger changes so we can discuss the direction.

License

MIT β€” see LICENSE.md.

Adoption Assessment

A verified publisher signal is available. Assess fit from the package's supported frameworks, license and dependency graph rather than popularity alone.

This version is marked prerelease. Validate its API and upgrade path before production use.Compatibility metadata lists 1 target framework; confirm the exact target used by your application.

Related Guides & Workflows

Learn how to evaluate, integrate, or audit this package in your .NET project.