DEV Community

Cover image for Mocked data for learning
Karen Payne
Karen Payne

Posted on

Mocked data for learning

Introduction

When a developer is learning a new method or technique, data is generally required to populate a development instance of a database; this article explains how.

For all samples, a NuGet package Bogus was used to generate data. Each time a sample project runs, the data remains the same, although there is an option to randomize it.

Sample data generator usage

Basics

Rather than creating fictitious data in a project, instead, create a class project that contains classes to generate data into models that also exist in the class project.

There are several options for using the class project. Create a test project in the same Visual Studio solution, copy the class project to another Visual Studio solution, or create a local NuGet package that can then be used no differently than any other NuGet package.

Considerations for data

In general, some ideas, person and product classes are a great starting point.

Examples

A developer wants to learn how to filter and order data. Executing ProductGenerator.Create(15) creates a list of 15 products followed by filtering Products by the UnitPrice property greater than 100, then ordering by UnitPrice.

The developer's goal is to learn how to write the code and by having predefined data saves the developer time to focus on learning rather than generating mocked data.

private static void DisplayHighValueProducts()
{
    SpectreConsoleHelpers.PrintPink();

    IOrderedEnumerable<Products> products = ProductGenerator.Create(15)
        .Where(x => x.UnitPrice > 100)
        .OrderByDescending(x => x.UnitPrice);

    foreach (var p in products)
    {
        AnsiConsole.MarkupLine($"[bold green]{p.ProductName,-25}[/][yellow]{p.UnitPrice:C}[/]");
    }
}
Enter fullscreen mode Exit fullscreen mode

What if a predefined class does not fit when they need data? An option is to create a new class that uses an implicit operator as shown below.

public class Products
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public int? CategoryId { get; set; }
    public decimal? UnitPrice { get; set; }
    public short? UnitsInStock { get; set; }
    public virtual Categories Category { get; set; }
    public string CategoryName => Category.CategoryName;
    public override string ToString() => ProductName;
}

public class ProductItem
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal? UnitPrice { get; set; }
    public override string ToString() => Name;

    public static implicit operator ProductItem(Products product) =>
        new()
        {
            Id = product.ProductId,
            Name = product.ProductName,
            UnitPrice = product.UnitPrice
        };
}
Enter fullscreen mode Exit fullscreen mode

Example for implicit operator

List<ProductItem> products = ProductGenerator.Create(10)
    .Select<Products, ProductItem>(p => p)
    .ToList();
Enter fullscreen mode Exit fullscreen mode

Anatomy of a generator

A generator, in this case, has a method to create a list of the desired model and a single instance of the model using the NuGet Bogus package.

public class ProductGenerator
{

    public static List<Categories> GeneratedCategories { get; private set; } = [];

    public static List<Products> Create(int count, bool random = false)
    {
        if (count <= 0)
            return [];

        // Seed control for reproducibility vs full randomness
        Seed = !random ? new Random(338) : null;

        // 1. Generate some categories
        //    Adjust categoryCount as you like or make it a parameter later.
        const int categoryCount = 5;

        var categoryFaker = new Faker<Categories>()
            .StrictMode(true)
            .RuleFor(c => c.CategoryId, f => f.IndexFaker + 1) // 1..categoryCount
            .RuleFor(c => c.CategoryName, f => f.Commerce.Categories(1).First())
            .RuleFor(c => c.Products, f => new HashSet<Products>());

        GeneratedCategories = categoryFaker.Generate(categoryCount);

        // 2. Generate products and link them too categories
        var productFaker = new Faker<Products>()
            .StrictMode(true)
            .RuleFor(p => p.ProductId, f => f.IndexFaker + 1)
            .RuleFor(p => p.ProductName, f => f.Commerce.ProductName())
            .RuleFor(p => p.CategoryId, f => f.PickRandom(GeneratedCategories).CategoryId)
            .RuleFor(p => p.UnitPrice, f => decimal.Parse(f.Commerce.Price(1, 500)))
            .RuleFor(p => p.UnitsInStock, f => (short)f.Random.Int(0, 500))
            .RuleFor(p => p.Category, (f, p) => GeneratedCategories.First(c => c.CategoryId == p.CategoryId));

        var products = productFaker.Generate(count);

        // 3. Back-fill the Category.Products collections so navigation
        //    works both ways in memory.
        foreach (var category in GeneratedCategories)
        {
            var catProducts = products
                .Where(p => p.CategoryId == category.CategoryId)
                .ToList();

            // Categories constructor already initializes Products to HashSet<Products>,
            // but this makes sure it reflects the generated list.
            category.Products = new HashSet<Products>(catProducts);
        }

        return products;
    }

    public static Categories CreateOne(bool random = false)
        => Create(1, random).FirstOrDefault()!.Category;
}
Enter fullscreen mode Exit fullscreen mode

Careful consideration should be given when creating generators. For instance a Human model has a property of Address. There may be times when the Address model needs to have data generated outside of a Human. In this case unlike the products generator there is a case for an address generator.

public static class AddressGenerator
{
    public static List<Address> Create(int count = 1)
    {

        Randomizer.Seed = new Random(337);

        var faker = new Faker<Address>()
            .RuleFor(a => a.Id, f => f.IndexFaker + 1)
            .RuleFor(a => a.Street, f => f.Address.StreetName())
            .RuleFor(a => a.City, f => f.Address.City())
            .RuleFor(a => a.State, f => f.Address.State())
            .RuleFor(a => a.ZipCode, f => f.Address.ZipCode())
            .RuleFor(a => a.Country, f => f.Address.Country());

        return faker.Generate(count);
    }

    public static Address CreateOne()
        => Create().FirstOrDefault()!;
}
Enter fullscreen mode Exit fullscreen mode

Summary

Having data generators allows a developer to focus on learning rather than setting up mocked data.

Another use for data generators is for seeding databases. EF Core has UseAsyncSeeding for data seeding which a data generator can be use along with a property in appsettings.json to determine to use a data generator.

Source code

Source code

  • All code is fully documented
  • Has five generators
  • There are two console projects which uses the generators
  • For each generator has methods to serialize to JSON which can be helpful in simulating deserializing from a file.

Top comments (0)