TryAtSoftware.Randomizer
is a library that should simplify the process of random values generation.
We offer a set of methods and components that can be used to generate random values of different types. They are reusable and can be applied to every projects of yours.
Try At Software
is a software development company based in Bulgaria. We are mainly using dotnet
technologies (C#
, ASP.NET Core
, Entity Framework Core
, etc.) and our main idea is to provide a set of tools that can simplify the majority of work a developer does on a daily basis.
Before creating any randomizers, you need to install the package.
The simplest way to do this is to either use the NuGet package manager
, or the dotnet CLI
.
Using the NuGet package manager
console within Visual Studio, you can install the package using the following command:
Install-Package TryAtSoftware.Randomizer
Or using the dotnet CLI
from a terminal window:
dotnet add package TryAtSoftware.Randomizer
Most of the necessary methods for generating primitive values are contained within the RandomizationHelper
class.
In the next sections you can read more about every one of them.
There are multiple methods that can be used to generate random integers:
RandomInteger
generates a random 32-bit signed integer within a given range.RandomUnsignedInteger
generates a random 32-bit unsigned integer within a given range.RandomLongInteger
generates a random 64-bit signed integer within a given range.RandomUnsignedLongInteger
generates a random 64-bit unsigned integer within a given range.
If no parameters are provided, no range constraints will be applied when generating the random integer.
// The next line will generate a random 32-bit signed integer in range: [int.MinValue, int.MaxValue]
int randomInteger = RandomizationHelper.RandomInteger();
If two numbers are provided (inclusive lower bound and exclusive upper bound), the generated integer will be in the [inclusive_lower_bound, exclusive_upper_bound)
range.
As the provided upper bound is exclusive, it will equal one more than the greatest value that can be generated.
// The next line will generate a random 64-bit unsigned integer in range: [0, 100)
// NOTE: The upper bound is exclusive, so the maximum value that can be generated is 100 - 1 = 99.
ulong randomInteger = RandomizationHelper.RandomUnsignedLongInteger(0UL, 100UL);
This overload is very convenient whenever a random index should be generated.
int[] array = new int[100];
for (int i = 0; i < array.Length; i++) array[i] = i;
var randomIndex = RandomizationHelper.RandomInteger(0, array.Length);
If the upper bound should be inclusive, we suggest using the third overload - it accepts an additional boolean value defining whether or not the upper bound should be interpreted as exclusive or inclusive.
// NOTE: All statements in the following example are equivalent!
// The next line will generate a random 32-bit unsigned integer in range: [1, 1000]
// NOTE: The upper bound is inclusive, so it equals the maximum value that can be generated.
long randomInteger1 = RandomizationHelper.RandomLongInteger(1, 1000, upperBoundIsExclusive: false);
long randomInteger2 = RandomizationHelper.RandomLongInteger(1, 1001, upperBoundIsExclusive: true);
long randomInteger2 = RandomizationHelper.RandomLongInteger(1, 1001);
If only one of the bounds is known, you can use
T.MinValue
orT.MaxValue
as fillers (whereT
is the corresponding numeric type).
Most of the existing methods for generating random numerical values do not include T.MaxValue
(where T
is the corresponding numeric type).
We have noticed that this limitation is completely unnecessary and it is isolated from our code base.
// The next line will generate a random 64-bit signed integer in range: [0, long.MaxValue]
long randomInteger = RandomizationHelper.RandomLongInteger(0, long.MaxValue, upperBoundIsExclusive: false);
We cannot use the logic from the previous section as
T.MaxValue + 1
will cause an overflow.
There are two methods that can be used - RandomDouble
and RandomFloat
.
They both have the same characteristics and will generate a random floating-point value of the corresponding type within the range [0, 1)
.
double randomDouble = RandomizationHelper.RandomDouble();
float randomFloat = RandomizationHelper.RandomFloat();
The RandomBytes
method can be very useful when we work with byte arrays, streams, files. etc.
It will generate an array of random byte values by a given length.
// When working with files:
byte[] fileContent = RandomizationHelper.RandomBytes(length: 1024);
await File.WriteAllBytesAsync("path/to/file", fileContent, cancellationToken);
// When working with streams:
byte[] streamContent = RandomizationHelper.RandomBytes(length: 1024);
await using MemoryStream stream = new MemoryStream(streamContent);
await UploadContentAsync(stream, cancellationToken);
It is quite often necessary to have a mechanism of generating a random boolean value.
The method we can use in this case is called RandomProbability
.
For example, when making random changes to some elements of a given array - for each index we can generate a random bool
denoting if a change should be made.
int[] array = new int[100];
for (int i = 0; i < array.Length; i++)
{
if (RandomizationHelper.RandomProbability()) array[i] = RandomizationHelper.RandomInteger();
else array[i] = i;
}
This method accepts an optional parameter called percents
.
It can be used to specify the likelihood of the generated value being true
.
// The generated value will be `true` in 1% of the cases (respectively `false` in the other 99%).
bool rarelyTrue = RandomizationHelper.RandomProbability(percents: 1);
// The generated value will be `true` in 99% of the cases (respectively `false` in the other 1%).
bool rarelyFalse = RandomizationHelper.RandomProbability(percents: 99);
Along with generating numbers, this is one of the most commonly used methods for randomizing primitive values. In practice, software developers work with text incessantly. We often have to use classes exposing some of the following properties - name, description, address, note, etc.
In this section, you can review some standard applications of the GetRandomString
methods.
By default, the GetRandomString
method will return a newly generated sequence of characters (letters from the Latin alphabet in lower and upper case and all digits) with random length (in rhe range [30, 80]
).
string randomText = RandomizationHelper.GetRandomString();
When randomizing text, there are two parameters than could be adjusted - its length and the list of characters to use.
There are some overloads of the GetRandomString
method that support this.
Moreover, the RandomizationHelper
class exposes some constants for the most common groups of characters to use when generating random text.
string randomText = RandomizationHelper.GetRandomString(length: 20, charactersMask: RandomizationHelper.UPPER_CASE_LETTERS);
If the same character is included multiple times within the characters mask, its probability of being used increases proportionally.
Just after text and numbers, the other most common type of primitive values developers use on a daily basis, are date and time values. We often have to use classes exposing some of the following properties - created at, modified at, birthday, time of arrival, etc.
The GetRandomDateTimeOffset
method can be used to generated random DateTimeOffset
values in the past or in the future (relative to the time of generation).
var pastDateTimeOffset = RandomizationHelper.GetRandomDateTimeOffset(historical: true);
// NOTE: The `historical` parameter has a default value of `false`, so it can be safely omitted.
var futureDateTimeOffset = RandomizationHelper.GetRandomDateTimeOffset(historical: false);
To define a randomizer, you need to create a class that implements the IRandomizer<T>
interface, where T
is the type of information this component should be responsible for randomizing.
For example, imagine that you want to create a randomizer that generates a random DateTime
instance in the future.
You would need the following class:
using System;
using TryAtSoftware.Randomizer.Core.Helpers;
using TryAtSoftware.Randomizer.Core.Interfaces;
public class DateTimeRandomizer : IRandomizer<DateTime>
{
private readonly DateTime _instantiationTime = DateTime.Now;
public DateTime PrepareRandomValue()
{
// We want to generate a random DateTime instance that is a few minutes ahead of our current time.
var randomOffsetInMinutes = RandomizationHelper.RandomInteger(15, 60);
return this._instantiationTime.AddMinutes(randomOffsetInMinutes);
}
}
Then you can use this randomizer as follows:
var dateTimeRandomizer = new DateTimeRandomizer();
for (int i = 0; i < 5; i++)
{
var randomDateTime = dateTimeRandomizer.PrepareRandomValue();
Console.WriteLine(randomDateTime);
}
If you run this code, you will see that the output is a few random DateTime
instances.
You may notice that the output sometimes may contain a value more than once.
This is because the strength
of our randomizer is not great.
If we analyze it more deeply, we can see that it can generate only 45 different values by design.
We will discuss various mechanisms to increase the strength
of our randomizers in some of the next sections.
As you saw in the previous chapter, writing standard randomizers for simple types is easy - you just implement a single method and that's it.
Randomizing complex types is not a more complex operation. We have provided you whit a base class that you can use to implement your own complex randomizers.
It is called ComplexRandomizer<T>
where T
is the complex type it should be responsible for randomizing.
Let's start with a basic example! For simplicity, we will work with this model:
using System;
public class Person
{
public Guid Id { get; set; }
public string Name { get; set; }
public bool IsEmployed { get; set; }
public int Age { get; set; }
public DateTimeOffset EventDate { get; set; }
}
Our complex randomizer will look like this:
using TryAtSoftware.Randomizer.Core;
using TryAtSoftware.Randomizer.Core.Primitives;
public class PersonRandomizer : ComplexRandomizer<Person>
{
public PersonRandomizer()
{
this.Randomize(p => p.Id, new GuidRandomizer());
this.Randomize(p => p.Name, new StringRandomizer());
this.Randomize(p => p.Age, new NumberRandomizer());
this.Randomize(p => p.IsEmployed, new BooleanRandomizer());
this.Randomize(p => p.EventDate, new DateTimeOffsetRandomizer());
}
}
Let's examine this complex randomizer!
We can see that it can be used to define a specific randomizer for each publicly exposed member of the Person
type.
And actually this is the main idea of our library. This way we can reuse already defined randomizers for different types.
We can combine them to create complex randomizers that can generate random instances of any type. Isn't that great?!
If we want to examine the complex randomization process more deeply, we can see that it is contained of two phases that we will describe in the next sections.
The IInstanceBuilder<T>
is a component responsible for instantiating the complex type.
By default, a GeneralInstanceBuilder<T>
will be used if none is specified.
This is the recommended option of instance building as it is implemented to find the most suitable constructor and invoke it (so you do not have to write any additional code).
Furthermore, it tracks down the used parameters so the randomization rules defining them will not be used during the next phase of the complex randomization process.
However, if you want to implement one by yourself, you have a few options:
- You can implement the
IInstanceBuilder<Person>
interface and define some custom instantiation logic. If you chose this option, you are in full control of the instantiation process.
using TryAtSoftware.Randomizer.Core;
using TryAtSoftware.Randomizer.Core.Interfaces;
public class PersonInstanceBuilder : IInstanceBuilder<Person>
{
public IInstanceBuildingResult<Person> PrepareNewInstance(IInstanceBuildingArguments arguments)
{
// You can use the provided instance building arguments to customize the instance.
// Note that if you do so, the returned instance building result may need some changes.
var person = new Person();
return new InstanceBuildingResult<Person>(person);
}
}
- You may inherit the
SimpleInstanceBuilder<T>
class and implement thePrepareNewInstance
method. This option should be used for really simple instance building that does not require any additional parameters being provided to the constructor. It is most suitable for complex types that have publicly exposed non-read-only members.
using TryAtSoftware.Randomizer.Core;
public class PersonInstanceBuilder : SimpleInstanceBuilder<Person>
{
protected override Person PrepareNewInstance() => new Person();
}
In this phase the SetValue
method for the value setter of each registered randomization rule will be invoked.
The order will be preserved - the value setter of a given randomization rule will be invoked before the value setter of a randomization rule that is registered after the given.
- We do not recommend using
complex randomizers
with abstract classes or interfaces. While it is possible to make such a setup work, there may be some intricacies along the way. Another idea that is especially useful when dealing with many derived types that have behavioral but not structural differences, is to make thecomplex randomizer
generic.
For additional information on troubleshooting, migration guides, answers to Frequently asked questions (FAQ), and more, you can refer to the Wiki pages of this project.