Link Search Menu Expand Document

2. Sample Data

In Module 01 the database started empty on every run. This module loads sample data from CSV files embedded in Autobarn.Data. When the app starts, it has 90 makes, 1,059 models and 5,001 vehicles.

What’s in this module

The solution is the same as Module 01, plus a Sample folder and a helper class in Autobarn.Data:

Autobarn.Data/
├── Autobarn.Data.csproj         (changed)
├── AutobarnDbContext.cs         (changed)
├── EmbeddedResource.cs          (new)
└── Sample/
    ├── SampleData.cs            (new)
    ├── makes.csv                (new: 90 rows)
    ├── models.csv               (new: 1,059 rows)
    └── vehicles.csv             (new: 5,001 rows)

Nothing in Autobarn.Website changed. The website shows the data without any changes because seeding happens inside the DbContext, and Program.cs already calls EnsureCreatedAsync().

Changes from Module 01

File Change
Autobarn.Data/Sample/makes.csv, models.csv, vehicles.csv Added. Raw sample data
Autobarn.Data/Autobarn.Data.csproj Modified. Embeds the CSV files in the assembly
Autobarn.Data/EmbeddedResource.cs Added. Helpers for reading embedded resources
Autobarn.Data/Sample/SampleData.cs Added. Parses the CSV files into objects
Autobarn.Data/AutobarnDbContext.cs Modified. Seeds makes and models with HasData, and vehicles with UseSeeding

Step 1: Add the CSV files

Files: Autobarn.Data/Sample/*.csv

The files are plain CSV with no header row and no quoting:

# makes.csv          code,name
abarth,ABARTH
aixam,AIXAM
alfa-romeo,ALFA ROMEO

# models.csv         code,makeCode,name
abarth-124,abarth,124
abarth-500,abarth,500

# vehicles.csv       registration,modelCode,color,year
AA07AMM,nissan-note,Turquoise,2007
AAC792H,hyundai-i10,Silver,1975

The columns match the entity properties from Module 01, and the codes use the natural keys (nissan, nissan-note).

Step 2: Embed the CSV files in the assembly

File: Autobarn.Data/Autobarn.Data.csproj

<ItemGroup>
	<EmbeddedResource Include="Sample\*.csv" />
</ItemGroup>

Why: An embedded resource is compiled into Autobarn.Data.dll. The data then goes wherever the assembly goes, whether that’s the website, a test project or a deployment, and nobody has to copy files or work out paths at runtime.

Step 3: A helper for reading embedded resources

File: Autobarn.Data/EmbeddedResource.cs (new)

public static class EmbeddedResource {

	public static Stream OpenStream(string resourceFileName, Assembly? assembly = null) {
		assembly ??= typeof(EmbeddedResource).Assembly;
		var name = assembly.GetManifestResourceNames()
			.FirstOrDefault(n => n.EndsWith(resourceFileName, StringComparison.OrdinalIgnoreCase));
		return (name is null ? null : assembly.GetManifestResourceStream(name))
			?? throw new FileNotFoundException($"Embedded resource not found in {assembly.GetName().Name}", resourceFileName);
	}

	public static string ReadAllText(string resourceFileName, Assembly? assembly = null) {
		using var reader = new StreamReader(OpenStream(resourceFileName, assembly));
		return reader.ReadToEnd();
	}

	public static string[] ReadAllLines(string resourceFileName, Assembly? assembly = null)
		=> ReadAllText(resourceFileName, assembly).ReplaceLineEndings().Split(Environment.NewLine);

	/// <summary>Reads a simple (unquoted) CSV resource, skipping any line that doesn't have exactly <paramref name="columns"/> fields.</summary>
	public static IEnumerable<string[]> ReadCsvData(string resourceFileName, int columns, Assembly? assembly = null)
		=> ReadAllLines(resourceFileName, assembly)
			.Select(line => line.Split(','))
			.Where(items => items.Length == columns);

	// ...plus ReadBytes()
}
  • MSBuild names embedded resources with the default namespace and folder, so Sample\makes.csv becomes Autobarn.Data.Sample.makes.csv. OpenStream matches with EndsWith, which lets callers just ask for "makes.csv".
  • ReplaceLineEndings() turns CRLF and LF into Environment.NewLine before splitting. Parsing then works however Git checked the files out.
  • ReadCsvData drops any line without the expected number of fields. That quietly skips blank lines, such as a trailing newline at the end of a file.

Step 4: Parse the CSV into objects

File: Autobarn.Data/Sample/SampleData.cs (new)

public static class SampleData {

	public record CarModelCsvRecord(string Code, string MakeCode, string Name);
	public record CarMakeCsvRecord(string Code, string Name);
	public record VehicleCsvRecord(string Registration, string ModelCode, string Color, int Year);

	public static IEnumerable<CarModelCsvRecord> VehicleModelData
		=> EmbeddedResource.ReadAllLines("models.csv", typeof(SampleData).Assembly)
			.Select(line => line.Split(","))
			.Where(tokens => tokens.Length == 3)
			.Select(tokens => new CarModelCsvRecord(tokens[0], tokens[1], tokens[2]));

	public static IEnumerable<CarMakeCsvRecord> VehicleMakeData
		=> EmbeddedResource.ReadAllLines("makes.csv", typeof(SampleData).Assembly)
			.Select(line => line.Split(","))
			.Where(tokens => tokens.Length == 2)
			.Select(tokens => new CarMakeCsvRecord(tokens[0], tokens[1]));

	public static IEnumerable<Vehicle> Vehicles
		=> EmbeddedResource.ReadCsvData("vehicles.csv", 4)
			.Select(tokens => new Vehicle {
				Registration = tokens[0],
				ModelCode = tokens[1],
				Color = tokens[2],
				Year = int.Parse(tokens[3], CultureInfo.InvariantCulture)
			});
}

Makes and models become small records. Vehicles become real Vehicle entities. The difference comes from how each one is seeded in the next step.

int.Parse uses CultureInfo.InvariantCulture, so parsing doesn’t depend on the machine’s regional settings. That matters at a workshop where attendees’ laptops use many different locales.

Step 5: Seed the database

File: Autobarn.Data/AutobarnDbContext.cs

using Autobarn.Data.Entities;
using Autobarn.Data.Sample;               // new
using Microsoft.EntityFrameworkCore;

At the end of OnModelCreating:

modelBuilder.Entity<VehicleMake>().HasData(SampleData.VehicleMakeData);
modelBuilder.Entity<VehicleModel>().HasData(SampleData.VehicleModelData);

A new OnConfiguring override:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
	optionsBuilder
		.UseSeeding((dbContext, _) => {
			if (dbContext.Set<Vehicle>().Any()) return;
			dbContext.AddRange(SampleData.Vehicles);
			dbContext.SaveChanges();
		})
		.UseAsyncSeeding(async (dbContext, _, cancellationToken) => {
			if (await dbContext.Set<Vehicle>().AnyAsync(cancellationToken)) return;
			await dbContext.AddRangeAsync(SampleData.Vehicles, cancellationToken);
			await dbContext.SaveChangesAsync(cancellationToken);
		});
}

The code uses both of EF Core’s seeding mechanisms, each for a different kind of data.

HasData for makes and models (reference data). HasData puts the seed rows into the model itself, and EnsureCreated inserts them together with the schema. It accepts any object whose property names match the entity, which is why the anonymous-style CarMakeCsvRecord and CarModelCsvRecord records work. They don’t need navigation properties. Makes and models form a small, fixed catalogue, which suits HasData.

UseSeeding / UseAsyncSeeding for vehicles (bulk, changeable data). These hooks, added in EF Core 9, run custom code after EnsureCreated or Migrate. There are about 5,000 vehicles, and new ones get added at runtime through the Advertise form. Model-level seed data would bloat the compiled model for no benefit. The seeding code checks Any() first so that it only runs once. EF Core recommends providing both the sync and async versions: Program.cs calls EnsureCreatedAsync(), which runs UseAsyncSeeding, and tools that call the sync API run UseSeeding.

Running it

dotnet run --project Autobarn.Website

Browse to /Makes, /CarModels or /Vehicles. The pages that showed “No makes found” in Module 01 now show data.

What’s next

The data is only available as HTML pages for people to read. Module 03 exposes it as JSON through a simple HTTP API.