MediocreDataManager 0.9.2

dotnet add package MediocreDataManager --version 0.9.2
                    
NuGet\Install-Package MediocreDataManager -Version 0.9.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MediocreDataManager" Version="0.9.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MediocreDataManager" Version="0.9.2" />
                    
Directory.Packages.props
<PackageReference Include="MediocreDataManager" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MediocreDataManager --version 0.9.2
                    
#r "nuget: MediocreDataManager, 0.9.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MediocreDataManager@0.9.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MediocreDataManager&version=0.9.2
                    
Install as a Cake Addin
#tool nuget:?package=MediocreDataManager&version=0.9.2
                    
Install as a Cake Tool

MediocreDataManager

A NuGet-packable ASP.NET Core library (net10.0) that drops a self-contained test-data management subsystem into a consuming app: middleware, a JSON HTTP API, an embedded Razor Pages admin UI, and an abstract EF Core MediocreDataManagerDbContext.

You define how your test data is generated by deriving from a small base class; the library handles persistence, an asynchronous generation queue with a dynamic worker pool, the admin UI for requesting/generating/handing out/downloading that data, and an API for consuming it from your tests.

Install

dotnet add package MediocreDataManager

How it fits together

The library's MediocreDataManagerDbContext is abstract. Your app always provides exactly one concrete subclass — that is how the library both gets a strongly-typed DbContextOptions<T> and discovers your context at startup (a reflection scan of the dependency graph). You never register the subclass yourself; ConfigureMediocreDataManager finds it, registers it with EF Core, and also forwards the base type so library code resolves the same scoped instance.

1. Define your DbContext

You have two shapes, depending on whether your app needs its own tables alongside the library's.

a) Vanilla — just the library's tables

The minimal case is an empty subclass. No DbSets, no OnModelCreating override needed — you only subclass so the library has a concrete type to register and discover.

using Microsoft.EntityFrameworkCore;
using MediocreDataManager.Data;

public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
    : MediocreDataManagerDbContext(options);

b) Extended — add your own tables

Add DbSets for your own entities and override OnModelCreating. You must call base.OnModelCreating(modelBuilder) so the library's entities (under the tdm schema) are still mapped; then configure your own. Your tables live in the same database/context as the library's.

using Microsoft.EntityFrameworkCore;
using MediocreDataManager.Data;

public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
    : MediocreDataManagerDbContext(options)
{
    // Your application's own tables, alongside the library's tdm.* tables.
    public DbSet<Widget> Widgets => Set<Widget>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);   // REQUIRED — maps the library's entities first

        modelBuilder.Entity<Widget>(entity =>
        {
            entity.HasKey(x => x.Id);
            entity.Property(x => x.Name).HasMaxLength(128).IsRequired();
        });
    }
}

2. Implement one or more scenarios

A scenario implementation produces one test-data record at a time. Derive from ScenarioImplementation<TOutputType>: TOutputType is the shape of a generated record (a string is stored verbatim; anything else is JSON-serialized for storage). Name and Description are what the admin UI displays for the scenario — they live in code, right next to the generator, not in the database. Constructor injection works (implementations are created via ActivatorUtilities: once at startup to read the name/description, and once per generation task).

using MediocreDataManager.ScenarioImplementations;

public sealed class UsOrder(IMyOrderFactory factory) : ScenarioImplementation<Order>
{
    public override string Name => "US Order";

    public override string Description => "A new order for a US customer, serialized as JSON.";

    public override Order GenerateData(Guid requirementGenerationRequestId)
        => factory.NewOrder("US");   // stored JSON-serialized
}

When the generator draws from a set of inputs, derive from ScenarioImplementation<TOutputType, TInputType> instead: TInputType describes the input-parameter objects. Pull one input per record with GetNextInputParameter(requirementGenerationRequestId): inputs are handed out round-robin, with an isolated rotation per generation request, so several requests running against the same requirement never advance each other's cursor. GetInputParameters(requirementGenerationRequestId) returns the request's full set.

Each generation request resolves its input set once, at the start of the request, from one of three sources (highest precedence first):

  1. Per-request override — tick "Override input data for this request" in the "Generate New Data" modal and upload a file or paste a JSON/CSV blob; it applies to that request only.
  2. Requirement input data — upload/paste JSON or CSV when creating or editing the requirement; it applies to all of the requirement's generation requests.
  3. Code-defined — override DefineInputParameters() (optional — the default is empty). It runs once per generation request, so it may compute the set dynamically.

User-supplied data is validated against TInputType before it is stored: JSON must be an array of records carrying exactly the input type's properties; CSV needs a header row naming them (case-insensitive, any column order). For simple input types (string, numbers, Guid, dates, enums), JSON is an array of values and CSV is one value per line with no header.

using MediocreDataManager.ScenarioImplementations;

public sealed class CanadaOrder(IMyOrderFactory factory)
    : ScenarioImplementation<Order, CustomerSeed>
{
    public override string Name => "Canada Order";

    public override string Description => "A new order for a Canadian customer, serialized as JSON.";

    protected override IEnumerable<CustomerSeed> DefineInputParameters()
        => [new("CA-1001"), new("CA-1002"), new("CA-1003")];

    public override Order GenerateData(
        Guid requirementGenerationRequestId, CustomerSeed? inputParameters = null)
    {
        // An explicitly supplied seed (a direct call from your own code) wins; the library never
        // passes one, so background generation draws from the request's round-robin rotation.
        var seed = inputParameters ?? GetNextInputParameter(requirementGenerationRequestId);
        return factory.NewOrder("CA", seed.CustomerId);   // stored JSON-serialized
    }
}

Discovered implementations are upserted into the tdm.ScenarioImplementations table at startup; the admin UI lists them on the Scenarios page, and you declare Requirements directly against one.

3. Wire it up in Program.cs

ConfigureMediocreDataManager registers everything (DI, discovery, the worker pool, the controllers, and the Razor Pages services). UseMediocreDataManager (the WebApplication overload) adds the middleware, maps the UI, and maps GET / → the requirements landing page. You still call MapControllers() to expose the HTTP API.

using MediocreDataManager.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.ConfigureMediocreDataManager(o =>
{
    // REQUIRED: the SQL Server connection string for the library's database.
    o.ConnectionString = builder.Configuration.GetConnectionString("MediocreDataManager");

    // Optional: create the schema at startup (dev/test hosts; use migrations for real apps).
    o.EnsureDatabaseCreated = true;

    // Optional: the admin UI's title (navbar brand + browser window title).
    o.UiTitle = "Acme Test Data";
});

var app = builder.Build();

app.UseMediocreDataManager();   // middleware + Razor Pages UI + GET "/" redirect
app.MapControllers();           // exposes the JSON HTTP API

app.Run();

All configuration options

ConfigureMediocreDataManager(Action<MediocreDataManagerOptions>) exposes:

Option Type Default Purpose
ConnectionString string? null Required. SQL Server connection string for the database holding the library's tables (the library registers the discovered context with the SQL Server provider for you).
EnsureDatabaseCreated bool false Create the schema at startup via EF Core's EnsureCreated, before any library service touches the tables. Intended for dev/test hosts — EnsureCreated never alters an existing database, so use migrations for anything real.
UiTitle string "Test Data Manager" Display title of the embedded admin UI: the top-navbar brand and the browser window title of every page.
Authentication MediocreDataManagerAuthenticationOptions disabled Opt-in OpenID Connect authentication for the embedded UI and API — see below.

Hosts that are not a WebApplication should call the IApplicationBuilder overload of UseMediocreDataManager() and then map the UI themselves (app.MapRazorPages()); the WebApplication overload does both for you. With authentication enabled such hosts must also add app.UseAuthentication() and app.UseAuthorization() between routing and endpoint mapping, and map the library's sign-out endpoint with endpoints.MapMediocreDataManagerSignOut() (a WebApplication does all of this automatically).

Authentication (optional)

By default everything is anonymous. To put the UI and API behind your organization's OIDC identity provider (Okta, Entra ID, Auth0, …), opt in from the same registration call:

builder.ConfigureMediocreDataManager(o =>
{
    o.ConnectionString = builder.Configuration.GetConnectionString("MediocreDataManager");

    o.Authentication.Enabled = true;                                      // the opt-in switch
    o.Authentication.Authority = "https://your-org.okta.com/oauth2/default";
    o.Authentication.ClientId = builder.Configuration["Tdm:ClientId"];
    o.Authentication.ClientSecret = builder.Configuration["Tdm:ClientSecret"];
    o.Authentication.InitialAdminEmails.Add("you@your-org.com");          // bootstrap admin(s)
});

When Enabled is true, Authority, ClientId, and ClientSecret are required — startup throws an exception listing anything missing. Register https://your-app/mediocre-data-manager/signin-oidc as the redirect URI with your provider (the callback path is namespaced so it never collides with your app's own OIDC handler; override it via Authentication.CallbackPath).

How it behaves once enabled:

  • An unauthenticated browser request to any embedded page is redirected to the provider (authorization-code flow + PKCE). API paths return 401 instead of a redirect — there is no machine-credential path (yet), so programmatic callers need a browser-established session. GET /ping deliberately stays anonymous as a liveness probe.
  • The signed-in user must exist on the Admin page's Users tab with Access on; Admin additionally gates edits on the Admin page (settings and user management). Everyone else lands on an access-denied page with a sign-out button.
  • The user's Email is read from the JWT's email claim by default (EmailClaimType), and is the identity key matched — case-insensitively — against the Users table. FullName is read from the name claim (FullNameClaimType), falling back to the email, and refreshes on every sign-in.
  • InitialAdminEmails are seeded as access+admin users at startup (only when no row with that email exists, so demoting one later sticks). They are optional only if an admin user already exists in the database — with zero admins nobody could ever grant access, so startup fails with a clear message.
  • The library registers its own named schemes (MediocreDataManager.Cookie / MediocreDataManager.OpenIdConnect) and never changes your app's default schemes. One caveat: if your app registered exactly one scheme and relied on ASP.NET Core's single-scheme default inference, set your defaults explicitly in AddAuthentication(...) once this feature is on.
  • The navbar shows the signed-in email and a Sign out button (cookie session only; it does not sign the user out of the provider).

Other Authentication options: Scopes (default openid profile email), CallbackPath, and RequireHttpsMetadata (default true; only relax for a plain-HTTP dev identity provider).

Admin UI

Bootstrap-styled server-rendered pages under /mediocre-data-manager/ui:

Page Route
Test Data Requirements (landing) /mediocre-data-manager/ui/requirements
Scenarios /mediocre-data-manager/ui/scenarios
Admin (Settings + Users tabs) /mediocre-data-manager/ui/admin

A scenario is one of your ScenarioImplementation<TOutputType> classes — the Scenarios page is a read-only list of what was discovered. You declare Requirements against a scenario (the choice is fixed once created), then ask for data per generation request from the requirement's detail page: the "Generate New Data" button opens a modal where you name the request and pick its quantity and type — Batch file (generate the full quantity up front, then download the whole dataset as JSON — or as CSV when the scenario's output type is flat) or Reservation buffer (keep a standing pool and hand out the oldest available record one at a time, auto-refilling as records are consumed). A requirement can hold any mix of requests. Generation runs asynchronously on a background worker pool whose size and lease length are editable live from the Admin page's Settings tab. The Admin page's Users tab manages who may use the app when authentication is enabled: an "Add User" button plus per-user Access (may use the app) and Admin (may edit the Admin page) toggle buttons.

Background tasks start switched off. On every app start the BackgroundTasksEnabled setting is forced to off — turn generation on from the Admin page (the toggle takes effect within a couple of seconds, no restart). Turning it off again lets in-progress tasks finish but starts nothing new.

HTTP API

All endpoints are under /mediocre-data-manager (mapped by your app.MapControllers() call):

Method & route Purpose
GET /ping Liveness check (also stamps the marker header).
GET /scenario-implementations List discovered implementations with their name/description (read-only; they mirror code).
GET /requirements List requirements (most-recently-active first).
POST /requirements/{id}/next-value Hand out the next value from an Ongoing pool.

Consuming an Ongoing pool: "Get Next Value"

This is the typical test-time call: a Reservation buffer generation request keeps a pool of generated records topped up, and your test grabs one. POST /requirements/{id}/next-value hands out the oldest available value across the requirement's Ongoing pools, stamps it used, and enqueues one refill so the pool it came from stays full.

Responses:

  • 200 OK — { "id": "...", "value": "...", "handedOutAtUtc": "..." }. value is the generated record, verbatim.
  • 409 Conflict — the pools are momentarily empty (generation is likely still in progress); retry shortly.
  • 400 Bad Request — the requirement has no Reservation buffer generation request (Batch-file data is delivered via download, not single-record hand-out).
  • 404 Not Found — no requirement with that id.
# Find the requirement id (or copy it from the admin UI), then take the next record:
curl -X POST https://localhost:5001/mediocre-data-manager/requirements/3f2a.../next-value
# 200 → {"id":"...","value":"{\"orderId\":1234,\"country\":\"CA\"}","handedOutAtUtc":"2026-06-28T12:00:00Z"}

From a test using HttpClient:

public sealed record NextValue(Guid Id, string Value, DateTime HandedOutAtUtc);

var response = await client.PostAsync(
    $"/mediocre-data-manager/requirements/{requirementId}/next-value", content: null);

if (response.StatusCode == HttpStatusCode.Conflict)
{
    // Pool was momentarily empty — its fill/refill tasks are usually still generating. Back off
    // and retry; if the 409 persists, check the request's status in the admin UI (a pool whose
    // records failed all generation attempts stays empty until you generate a new request).
}
response.EnsureSuccessStatusCode();

var next = await response.Content.ReadFromJsonAsync<NextValue>();
var order = JsonSerializer.Deserialize<Order>(next!.Value);   // the generated record, ready to use

Requirements

  • A SQL Server database (the library uses EF Core with the SQL Server provider; you supply only the connection string).
  • The admin UI loads Bootstrap 5.3.8 from a CDN (jsDelivr), so the host needs outbound network access at runtime for full styling (it renders unstyled otherwise). The HTTP API has no such dependency.
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.9.2 150 7/10/2026
0.9.1 126 7/6/2026
0.9.0 134 6/29/2026