Post

C# Object Generator

C# Object Generator

Random Object Generator for Testing CRUD Applications

Introduction

Most of the time, when developing a CRUD application, the required data is not available early enough to support UI development. As a result, developers often resort to the dirty approach: manually inserting data directly into the database.

While manual insertion can work in some cases, it quickly becomes impractical. For example:

  • When writing test cases to verify that inserts behave correctly
  • When you need to provide large data sets (e.g., lists of objects) as input
  • When you want repeatable and automated test data generation

There are many utilities available that solve some or all of these problems. However, the question remains: how can we build such a solution ourselves?


Background

I was looking for a way to populate objects on the fly with random data so I could use them to verify the integrity of my functions. I found a few approaches on Stack Overflow, but I decided to build my own solution from scratch—mainly to understand the problem space better.

What I ended up with is not a fully completed or optimized solution, but it lays a solid foundation for anyone interested in this topic.


Using the Code

The implementation is intentionally simple and consists of two main classes:

  • Invoker Responsible for creating strongly typed property setters using expression trees.

  • RandomObjectsGenerator<T> Generates random values based on property types and uses the Invoker to populate an object.


The Invoker Class

The Invoker class exposes a CreateSetter method, which returns an Action<T, object>. This delegate represents the actual setter for a given property on type T.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Invoker
{
    public static Action<T, object> CreateSetter<T>(PropertyInfo propertyInfo)
    {
        var targetType = propertyInfo.DeclaringType;
        var info = propertyInfo.GetSetMethod();
        Type type = propertyInfo.PropertyType;

        var target = Expression.Parameter(targetType, "t");
        var value = Expression.Parameter(typeof(object), "st");

        var condition = Expression.Condition(
            Expression.Equal(value, Expression.Constant(DBNull.Value)),
            Expression.Default(type),
            Expression.Convert(value, type)
        );

        var body = Expression.Call(
            Expression.Convert(target, info.DeclaringType),
            info,
            condition
        );

        var lambda = Expression.Lambda<Action<T, object>>(body, target, value);
        return lambda.Compile();
    }
}

This approach avoids reflection-based invocation at runtime and provides better performance and type safety.


The RandomObjectsGenerator<T> Class

This class is responsible for generating random values and assigning them to object properties using the setters produced by the Invoker.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RandomObjectsGenerator<T> where T : class, new()
{
    private static Random rand = new Random();

    private static List<long> randomLongNumbersList = new();
    private static List<int> randomIntNumbersList = new();
    private static List<DateTime> randomDateTimeList = new();

    // Randomization bounds
    private int minLongRandBound = 1;
    private long maxLongRandBound = long.MaxValue;

    private int minIntRandBound = 1;
    private int maxIntRandBound = int.MaxValue;

Random Value Generators

Each primitive type has its own generator method.

Integer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private int GetIntNumber()
{
    byte[] buf = new byte[8];
    rand.NextBytes(buf);

    int intRand = BitConverter.ToInt32(buf, 0);
    int value = Math.Abs(intRand % (minIntRandBound - maxIntRandBound)) + minIntRandBound;

    if (!randomIntNumbersList.Contains(value))
        randomIntNumbersList.Add(value);
    else
        return GetIntNumber();

    return value;
}

Long

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private long GetLongNumber()
{
    byte[] buf = new byte[8];
    rand.NextBytes(buf);

    long longRand = BitConverter.ToInt64(buf, 0);
    long value = Math.Abs(longRand % (minLongRandBound - maxLongRandBound)) + minLongRandBound;

    if (!randomLongNumbersList.Contains(value))
        randomLongNumbersList.Add(value);
    else
        return GetLongNumber();

    return value;
}

Decimal

1
2
3
4
5
6
7
8
9
10
11
12
13
private decimal GetDecimal()
{
    byte scale = (byte)rand.Next(29);
    bool sign = rand.Next(2) == 1;

    return new decimal(
        GetIntNumber(),
        GetIntNumber(),
        GetIntNumber(),
        sign,
        scale
    );
}

Boolean

1
2
3
4
private bool GetBool()
{
    return rand.Next(100) <= 50;
}

String

1
2
3
4
private string GetString()
{
    return Guid.NewGuid().ToString();
}

DateTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private DateTime GetDateTime()
{
    DateTime startingDate = DateTime.Now.AddYears(-2);
    int range = (DateTime.Today - startingDate).Days;

    DateTime value = startingDate
        .AddDays(rand.Next(range))
        .AddHours(rand.Next(0, 24))
        .AddMinutes(rand.Next(0, 60))
        .AddSeconds(rand.Next(0, 60))
        .AddMilliseconds(rand.Next(0, 999));

    if (!randomDateTimeList.Contains(value))
        randomDateTimeList.Add(value);
    else
        return GetDateTime();

    return value;
}

Byte

1
2
3
4
5
6
private byte GetByte()
{
    byte[] buffer = new byte[10];
    rand.NextBytes(buffer);
    return buffer[rand.Next(0, 9)];
}

Generating the Random Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static T GenerateRandomObject()
{
    var randObjGen = new RandomObjectsGenerator<T>();
    var setters = new Dictionary<string, Action<T, object>>();

    const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
    var properties = typeof(T).GetProperties(flags).ToList();

    foreach (var prop in properties)
        setters.Add(prop.Name, Invoker.CreateSetter<T>(prop));

    T obj = new T();

    var typedValueMap = new Dictionary<Type, Delegate>
    {
        { typeof(int), new Func<int>(() => randObjGen.GetIntNumber()) },
        { typeof(long), new Func<long>(() => randObjGen.GetLongNumber()) },
        { typeof(decimal), new Func<decimal>(() => randObjGen.GetDecimal()) },
        { typeof(bool), new Func<bool>(() => randObjGen.GetBool()) },
        { typeof(DateTime), new Func<DateTime>(() => randObjGen.GetDateTime()) },
        { typeof(string), new Func<string>(() => randObjGen.GetString()) },
        { typeof(byte), new Func<byte>(() => randObjGen.GetByte()) }
    };

    foreach (var setter in setters)
    {
        var type = properties
            .Where(p => p.Name == setter.Key)
            .Select(p => p.PropertyType)
            .FirstOrDefault();

        if (type != null && typedValueMap.ContainsKey(type))
            setter.Value(obj, typedValueMap[type].DynamicInvoke(null));
    }

    return obj;
}

Design Notes

Why Use a Delegate Dictionary?

.NET does not support switching directly on Type (e.g., switch(type)). Since all random generators are parameterless and return typed values, a dictionary mapping Type → Delegate allows dynamic invocation based on property type.

As long as the type is known, the appropriate delegate can be invoked using:

1
DynamicInvoke(null)

Why Use the Invoker?

For each PropertyInfo, we need a fast and reusable setter. The Invoker builds these setters once using expression trees and stores them in a dictionary for later use. This avoids repeated reflection and improves performance.


Notes & Limitations

This implementation is not complete or fully optimized, but it serves its intended purpose. Some important limitations include:

  • No support for nested objects Sub-objects must be generated separately and manually assigned.

  • Forced uniqueness for int, long, and DateTime This can cause issues with foreign key relationships when inserting generated data into a database.

  • No configuration or extensibility layer All generation logic is hardcoded.

I may enhance this implementation in the future if the need arises. For now, it fits my requirements, and I hope it proves useful to others as well.

This post is licensed under CC BY 4.0 by the author.