Bulk generating EntitySeed classes for use by Audacia.Seed

Tim HiltonTim Hilton
6 min read

Introduction

I recently installed Audacia.Seed onto a project for the first time. I’ve used it for setting up test data before, but have never set it up from scratch, and was pleased to find it was a pretty easy experience.

However, there was one fly in the ointment. When using Audacia.Seed with Entity Framework, entities which cannot be saved to the database by default need a bit of setup. For example, if new MyEntity() isn’t valid for saving to the database, Audacia.Seed requires the developer to create a class inheriting from EntitySeed<MyEntity> to tell it how to create a valid MyEntity which can be saved to the database. In the codebase I was working with, I suspect required properties are the main example of these requirements.

I wanted to find a way to bulk generate these EntitySeed<T> classes, as it would be tedious to write them all by hand. This post outlines how I approached the problem.

Finding required properties

All of the project’s Entity Framework IEntityTypeConfiguration classes are within a single directory, so I used Rider’s regex search within that directory to find required properties. The regex I used was:

\.Property\((.*)\)\s*\.IsRequired\(\)

This finds instances of the string .Property( followed by anything, followed by ) and some whitespace before the string .IsRequired(). Within the Rider Find window I could see the codebase has 123 required properties across all entities.

I selected all these results and copied them into a text document. (Note that Rider does have functionality to copy the search output into a text file but I prefer manually copying & pasting because it strips out data which isn’t useful to me, like the number of results in each folder and the line numbers of the code snippets.)

This gave me a text file which looked something like this (but longer):

Targets
Found usages
<Data Access>
<[REDACTED].EntityFramework>
Mappings
Accounts
AzureDevopsInstanceMap.cs
        builder.Property(azureDevopsInstance => azureDevopsInstance.Name)
        builder.Property(azureDevopsInstance => azureDevopsInstance.URL)
        builder.Property(azureDevopsInstance => azureDevopsInstance.AccessToken)
        builder.Property(azureDevopsInstance => azureDevopsInstance.ExpiryDate)
ClientMap.cs
        builder.Property(client => client.Name)
        builder.Property(client => client.IsActive)
Compliance
AssetMap.cs
        builder.Property(s => s.Name)
        builder.Property(s => s.AssetType)
        builder.Property(s => s.AssetStatus)

Note this text includes the name of the file and the list of required properties. Although this text file doesn’t say that these properties are required, we know they are because that’s how the initial search was defined.

Using Copilot to create EntitySeed classes

I manually created 2 EntitySeed classes, then used GitHub Copilot (Claude 3.7 Sonnet) with this prompt:

Within AssetSeed.cs, add more classes. There should be one for each *Map.cs file listed in required properties.txt. The class should be called *Seed and implement EntitySeed<*> where * comes from *Map.cs. e.g. AssetMap.cs should lead to AssetSeed : EntitySeed<Asset> (this example is already implemented). Within the newly generated *Seed class, override the GetDefault method. The list of properties it should define data for is the list of properties which is listed under the *Map.cs filename. Guess a reasonable data type for them, and default to string if you are not sure.

The output seemed pretty good, although it was truncated. I had 53 classes to do (containing 123 required properties), and Copilot managed 15 of them. I accepted its output (and deleted a class it had got halfway through trying to generate), then re-prompted it with:

Keep going for the remaining classes

Unfortunately this only generated one extra class, so I suspect the task is simply too big for the LLM to handle. I tried with a more specific prompt:

There are many classes in required properties.txt which have no equivalent in AssetSeed.cs. Identify these and create classes for them in AssetSeed.cs using the previously defined rules, and following the existing examples. Do not create duplicate classes.

This didn’t generate any meaningful new content, so I concluded that I’d hit the limits of what Copilot could handle (at least when using Claude 3.7 Sonnet).

At this point I went through the generated classes properly and was pretty pleased with the output. There were some errors, mainly when naming was inconsistent (e.g. a property called Status using an enum called StatusType) but it was quick work to manually fix them, and I was left with 18 classes, only 2 of which I’d written myself.

This was a good start but left 35 classes still to generate. I concluded that I would need to change tack in some way.

Splitting the input

Because Copilot had done a good job of generating the classes, I decided to try and keep using it but reducing the size of the input document. I initially rejected this idea because I’d misunderstood the count within the find window and was thinking I had 123 classes to generate (rather than 123 properties across 53 classes), so it could still be fairly time consuming to generate them 15 at a time. Once I realised my mistake, and that I only had 35 classes left, I thought I could probably generate the remainder of them using Copilot for another 2 or 3 iterations.

I took a copy of required properties.txt and deleted the content up to the most recent class which Copilot had generated. I then provided the updated file to Copilot with my original prompt, and it continued creating classes in AssetMap.cs. After this second iteration there were only a few classes remaining, so I repeated the process and got the full set.

Tidying up

Once the classes were all generated, I quickly fixed build errors where Copilot had guessed the wrong data type, and tidied up anything else I didn’t like about the generated code. This left me with one file containing the 53 classes I need, at which point I got Rider to move all the classes into their own files and committed them.

I also registered the assembly with Audacia.Seed using an assembly attribute, as explained in the documentation, then moved on to writing tests using these entities. So far, everything is working as expected and I’m very happy with the (low) amount of effort it took to generate this boilerplate code.

Alternative approaches I considered

Multiline editing

Given my text document, it would be easy to select all the entity names from it. I could then use Rider’s multiline editing functionality to write the outline of the EntitySeed class for each one. This would be a very fast way to generate all the classes, but I would need to manually go through each one individually in order to specify all the required properties.

It might have been possible to specify all the required properties with multiline editing too, I’m not sure, but I would probably have defaulted them all to strings and tidied up the data types afterwards. Without trying it, it’s hard to say whether or not this would have worked better than using Copilot did.

Write a program to write the code

I could use some find & replaces to convert my text file to json so it’s machine-readable, then write a console app which reads the json and writes the classes I want. This would both handle the generation of the classes I need, and also define the required properties. I would have to choose what default value to use for them as I do not know the data type of each property, then manually fix these afterwards. The approach of using Copilot has a similar limitation, as I wasn’t telling Copilot the data types, but it can guess the data type which I was hoping would mean there was less manual fixing to do afterwards (though I still expected to do some).

T4 template

In principle this is basically the same idea as writing a console application, just using a different technology. It’s a while since I’ve used T4 templates and I don’t need to be able to re-generate these classes in the future, so I decided not to use this approach.

1
Subscribe to my newsletter

Read articles from Tim Hilton directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tim Hilton
Tim Hilton