Creating mocked data for EF Core using Bogus and more

Karen Payne - Jun 11 - - Dev Community

Introduction

The main objective is to demonstrate creating data for Microsoft EF Core that is the same every time the application runs and/or unit test run.

The secondary objective is to show how to use IOptions and AddTransient to read information from appsettings.json which is not gone over completely so if interested see the following GitHub repository which has code samples for console and web projects.

Sample project

Walkthrough

In the source code, the first steps are to read the database connection string and a setting to decide to create a fresh copy of the database from appsettings.json.

appsetting.json

{
  "ConnectionStrings": {
    "MainConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Bogus2;Integrated Security=True;Encrypt=False",
    "SecondaryConnection": "TODO"
  },
  "EntityConfiguration": {
    "CreateNew": false
  }
}
Enter fullscreen mode Exit fullscreen mode

In Program.cs, the following code sets up to read appsettings.json data.

private static async Task Setup()
{
    var services = ApplicationConfiguration.ConfigureServices();
    await using var serviceProvider = services.BuildServiceProvider();
    serviceProvider.GetService<SetupServices>()!.GetConnectionStrings();
    serviceProvider.GetService<SetupServices>()!.GetEntitySettings();
}
Enter fullscreen mode Exit fullscreen mode

Both connection string and application settings use singleton classes to access information in various parts of the program.

public sealed class DataConnections
{
    private static readonly Lazy<DataConnections> Lazy = new(() => new DataConnections());
    public static DataConnections Instance => Lazy.Value;
    public string MainConnection { get; set; }
    public string SecondaryConnection { get; set; }
}

public sealed class EntitySettings
{
    private static readonly Lazy<EntitySettings> Lazy = new(() => new EntitySettings());
    public static EntitySettings Instance => Lazy.Value;
    /// <summary>
    /// Indicates if the database should be recreated
    /// </summary>
    public bool CreateNew { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Next, in Program.Main method

  • Instantiate an instance of the DbContext
  • EntitySettings.Instance.CreateNew determines if the database should be created fresh.
static async Task Main(string[] args)
{

    await Setup();
    await using var context = new Context();
    if (EntitySettings.Instance.CreateNew)
    {
        await context.Database.EnsureDeletedAsync();
        await context.Database.EnsureCreatedAsync();
    }

    . . .
}
Enter fullscreen mode Exit fullscreen mode

Database details.

There is one table BirthDays which

  • YearsOld column is a computed column
  • BirthDate is a date column, C# side a DateOnly
  • Gender is a string column, C# side an enum
CREATE TABLE [dbo].[BirthDays] (
    [Id]        INT            IDENTITY (1, 1) NOT NULL,
    [FirstName] NVARCHAR (MAX) NULL,
    [LastName]  NVARCHAR (MAX) NULL,
    [Gender]    NVARCHAR (MAX) NOT NULL,
    [BirthDate] DATE           NULL,
    [YearsOld]  AS             ((CONVERT([int],format(getdate(),'yyyyMMdd'))-CONVERT([int],format([BirthDate],'yyyyMMdd')))/(10000)),
    [Email]     NVARCHAR (MAX) NULL,
    CONSTRAINT [PK_BirthDays] PRIMARY KEY CLUSTERED ([Id] ASC)
);
Enter fullscreen mode Exit fullscreen mode

DbContext

OnConfiguring the connection is setup, read from DataConnections _ class and _DbContextToFileLogger is setup and responsible for logging all EF Core operations to a daily log file beneath the application.

OnModelCreating setup a conversion for Gender property and setup the computed column.

Note
The Gender enum is setup as the same as Gender in Bogus, more on this later.

Next, the final part determines if Bogus data should be used and will only happen when testing, not for production so be mindful to change the setting in appsetting.json before moving to production.

public partial class Context : DbContext
{
    public Context()
    {
    }

    public Context(DbContextOptions<Context> options)
        : base(options)
    {
    }

    public virtual DbSet<BirthDays> BirthDays { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        /*
         * Get connection string from appsetting.json
         * Setup logging to a file under the app folder (see the project file for folder creation)
         */
        optionsBuilder.UseSqlServer(DataConnections.Instance.MainConnection)
            .EnableSensitiveDataLogging()
            .LogTo(new DbContextToFileLogger().Log, new[]
                {
                    DbLoggerCategory.Database.Command.Name
                },
                LogLevel.Information);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<BirthDays>(entity =>
        {
            // setup enum conversion
            entity.Property(e => e.Gender)
                .HasConversion<int>()
                .IsRequired();

            // setup computed column
            entity.Property(e => e.YearsOld)
                .HasComputedColumnSql("((CONVERT([int],format(getdate(),'yyyyMMdd'))-CONVERT([int],format([BirthDate],'yyyyMMdd')))/(10000))", false);
        });

        if (EntitySettings.Instance.CreateNew)
        {
            modelBuilder.Entity<GenderData>().HasData(BogusOperations.GenderTypes());
            modelBuilder.Entity<BirthDays>().HasData(new List<BirthDays>(BogusOperations.PeopleList(20,338)));
        }


    }

}
Enter fullscreen mode Exit fullscreen mode

BirthDays model

There are two constructors, the overload which passes an int is for use when creating data with Bogus.

public partial class BirthDays
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    /// <summary>
    /// Person gender
    /// </summary>
    public Gender Gender { get; set; }

    public DateOnly? BirthDate { get; set; }

    // computered column, see DbContext OnModelCreating
    public int? YearsOld { get; set; }

    public string Email { get; set; }

    // For Bogus to set Id property
    public BirthDays(int id)
    {
        Id = id;
    }

    public BirthDays()
    {

    }
}
Enter fullscreen mode Exit fullscreen mode

Creating Bogus data

To create consistent data with Bogus, the Faker must be seeded using Randomizer.Seed = new Random(338) where 338 can be any number but in some cases the seed in tangent with count of instances of the (any) model may not align first names with the proper gender so play with the number if matching first names to the proper gender.

In the image below, the arrows mark the code which attempt to match first names with the proper gender.

Both methods produce the same results, its a matter of preference which to use.

Shows two methods to create bogus data

Third party NuGet packages uses

Package Purpose
Bogus For creating mocked data
ConfigurationLibrary Provides access to appsettings.json for connection strings for three environments, development, testing/staging and production.
EntityCodeFileLogger A simple class to log EF Core operations to a text file

EF Power Tools

EF Power Tools was used to reverse engineer the original database.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .