A library for easy generation of model classes
- Creating a model
- Populating a model
- Logging the build process
- Changing the model after creation
- Customizing the process
- Supporters
- Upgrading to 6.0.0
The static Model
class provides the entry point for generating models. It supports creating classes, structs and primitive types. It can also create complex object hierarchies.
var model = Model.Create<Person>();
You may want to create a model that ignores setting a property value.
var model = Model.Ignoring<Person>(x => x.FirstName).Create<Person>();
Ignoring a property can also be configured for types that may exist deep in an object hierarchy for the type being created.
var model = Model.Ignoring<Address>(x => x.AddressLine1).Create<Person>();
It also supports providing constructor arguments to the top level type being created.
var model = Model.Create<Person>("Fred", "Smith");
ModelBuilder will attempt to match constructor parameters to property values when building a new instance to avoid a new random value overwriting the property set by the constructor.
The following rules define how a match is made between a constructor parameter and a property value:
- No match if property type does not match the constructor parameter type (inheritance supported)
- No match if property value is the default value for its type
- Match reference types (except for strings) where the property value matches the same instance as the constructor parameter (using Object.ReferenceEquals)
- Match value types and strings that have the same value and the constructor parameter name is a case insensitive match to the property name
You may have an object instance that was created somewhere else. The Model
class can populate that for you.
var person = new Person
{
FirstName = "Jane"
};
var model = Model.Ignoring<Person>(x => x.FirstName).Populate(person);
var customer = new Person();
var customerModel = Model.Populate(customer);
ModelBuilder can write log messages while building values. The easiest way to output a build log is to use the WriteLog
extension method. Logging the build process is disabled by default however the WriteLog
extension method implicitly enables logging.
This would look like the following when using xUnit.
public class WriteLogTests
{
private readonly ITestOutputHelper _output;
public WriteLogTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void WriteLogRendersLogFromModel()
{
var actual = Model.WriteLog<Names>(_output.WriteLine).Create();
actual.Should().NotBeNull();
}
}
This test then outputs the generated build log to the xUnit test output.
Start creating type ModelBuilder.UnitTests.Models.Names using ModelBuilder.TypeCreators.DefaultTypeCreator
Start populating instance ModelBuilder.UnitTests.Models.Names
Creating property Gender (ModelBuilder.UnitTests.Models.Gender) on type ModelBuilder.UnitTests.Models.Names
Start creating type ModelBuilder.UnitTests.Models.Gender using ModelBuilder.ValueGenerators.EnumValueGenerator
End creating type ModelBuilder.UnitTests.Models.Gender
Created property Gender (ModelBuilder.UnitTests.Models.Gender) on type ModelBuilder.UnitTests.Models.Names
Creating property FirstName (System.String) on type ModelBuilder.UnitTests.Models.Names
Start creating type System.String using ModelBuilder.ValueGenerators.FirstNameValueGenerator
End creating type System.String
Created property FirstName (System.String) on type ModelBuilder.UnitTests.Models.Names
Creating property MiddleName (System.String) on type ModelBuilder.UnitTests.Models.Names
Start creating type System.String using ModelBuilder.ValueGenerators.MiddleNameValueGenerator
End creating type System.String
Created property MiddleName (System.String) on type ModelBuilder.UnitTests.Models.Names
Creating property LastName (System.String) on type ModelBuilder.UnitTests.Models.Names
Start creating type System.String using ModelBuilder.ValueGenerators.LastNameValueGenerator
End creating type System.String
Created property LastName (System.String) on type ModelBuilder.UnitTests.Models.Names
End populating instance ModelBuilder.UnitTests.Models.Names
End creating type ModelBuilder.UnitTests.Models.Names
Sometimes you need to tweak a model after it has been created. This can be done easily using the Set
extension method on any object.
var person = Model.Create<Person>()
.Set(x => x.FirstName = "Joe")
.Set(x => x.Email = null);
var otherPerson = Model.Create<Person>().Set(x =>
{
x.FirstName = "Joe";
x.Email = null;
});
The SetEach
method does the same as Set
but across enumerable objects.
var organisation = Model.Create<Organisation>();
organisation.Staff.SetEach(x => x.Email = null);
ModelBuilder is designed with extensibility in mind. There are many ways that you can customize the build configuration and the process of building models.
The extensibility points for customizing the build configuration are:
- IBuildConfiguration
- IConfigurationModule
- IExecuteStrategy
The extensibility points defined in IBuildConfiguration
that control how to create models are:
- IConstructorResolver
- ICreationRule
- IExecuteOrderRule
- IIgnoreRule
- IPostBuildAction
- IPropertyResolver
- ITypeCreator
- TypeMappingRule
- ITypeResolver
- IValueGenerator
One way to debug how custom extensibility types get resolved is to output the build log.
An IBuildConfiguration
instance provides access to all the configuration for creating values. The Model
class uses a default configuration that is built using the DefaultConfigurationModule
. You could however start with an empty BuildConfiguration
and modify the configuration from there.
var configuration = new BuildConfiguration();
configuration.Add(new MyCustomTypeCreator());
var value = configuration.Create<MyType>();
The items held by a build configuration can be modified for specific scenarios. For example, generating an Age value using the AgeValueGenerator
creates an age between 1 and 100 and generating sets of items using EnumerableTypeCreator
creates an enumerable set of data containing a random number of 10 and 30 between items. The logic of these can be modified before creating a value.
var model = Model.UsingDefaultConfiguration()
.UpdateValueGenerator<AgeValueGenerator>(x => x.MinAge = 18)
.Create<Person>();
var dataSet = Model.UsingDefaultConfiguration()
.UpdateTypeCreator<EnumerableTypeCreator>(x => x.MinCount = x.MaxCount = 10)
.Create<IEnumerable<Person>>();
An IConfigurationModule
defines a reusable configuration definition that is applied to IBuildConfiguration
. You can write your own by creating a class that implements IConfigurationModule.
public class MyCustomModule : IConfigurationModule
{
public void Configure(IBuildConfiguration configuration)
{
configuration.AddCreationRule<MyCreationRule>();
configuration.AddExecuteOrderRule<MyExecuteOrderRule>();
configuration.AddIgnoreRule<MyIgnoreRule>();
configuration.AddPostBuildAction<MyPostBuildAction>();
configuration.AddTypeCreator<MyTypeCreator>();
configuration.AddTypeMappingRule<MyTypeMappingRule>();
configuration.AddValueGenerator<MyValueGenerator>();
// Or
configuration.Add(new MyCreationRule());
configuration.Add(new MyExecuteOrderRule());
configuration.Add(new MyIgnoreRule());
configuration.Add(new MyPostBuildAction());
configuration.Add(new MyTypeCreator());
configuration.Add(new MyTypeMappingRule());
configuration.Add(new MyValueGenerator());
}
}
var model = Model.UsingModule<MyCustomModule>().Create<Person>();
You may want to create multiple configuration modules that support different model construction designs.
var model = Model.UsingModule<MyUnitTestModule>().Create<Person>();
var otherModel = Model.UsingModule<MyIntegrationTestModule>().Create<Person>();
You can also daisy-chain modules to build on top of other modules.
var model = Model.UsingDefaultConfiguration
.UsingModule<MyCustomModule>()
.UsingModule<MyOtherModule>()
.Create<Person>();
NOTE: The Model.UsingModule<T>()
method implicitly includes the DefaultConfigurationModule
which provides the configuration that Model.Create
and Module.Populate
uses.
Configuration modules are useful when you want to provide some consistent defaults for creating values. For example, you may have an Account entity with an IsActive boolean property and those should always be created with a value of true
rather than a random value.
public class TestModule : IConfigurationModule
{
/// <inheritdoc />
public void Configure(IBuildConfiguration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration.AddCreationRule<Account>(x => x.IsActive, true, 100)
}
}
You can then use this common build configuration across unit tests by referencing the module.
[Fact]
public void GetReturnsAccountWhenActiveIsTrue()
{
var account = Model.UsingModule<TestModule>.Create<Account>();
// account.IsActive will always be true
}
An IExecuteStrategy
provides the logic that creates a model instance from the configuration in a provided IBuildConfiguration
.
An IConstructorResolver
assists in resolving the constructor to execute when creating instances of a model.
The DefaultConstructorResolver
provides the logic for selecting the constructor with the least number of parameters unless constructor arguments have been supplied. In that case it selects a constructor that matches the argument list.
An ICreationRule
provides a simple implementation for returning a model value that bypasses the complexity of creating values using an IValueGenerator
or an ITypeCreator
. Most often they are used when creating a custom IBuildConfiguration
.
For example:
public class TestModule : IConfigurationModule
{
/// <inheritdoc />
public void Configure(IBuildConfiguration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration.AddCreationRule(typeof(bool), "^IsActive$", true, 100); // Make all boolean IsActive properties true across all types
configuration.AddCreationRule<Account>(x => x.IsReseller, false, 100);
configuration.AddCreationRule<MyCustomCreationRule>();
}
}
Implementing a custom IValueGenerator
or ITypeCreator
is the preferred method if generating values is more complex than assigning simple values.
Generating random or pseudo-random data for a model dynamically is never going to be perfect. We can however provide better data when some context is available. An IExecuteOrderRule
helps here by defining the order in which a property or parameter value is generated when the model is being created. This means that creating one value may then be dependent on another by controlling the order in which they are created.
For example, if a Person type exposes an Email, FirstName and LastName properties then the email value should include the first and last names. The DefaultConfigurationModule
supports this by defining that the execute order (descending integer priority) for this scenario is FirstName, LastName, Domain, Email and then other string properties or parameters.
configuration.AddExecuteOrderRule(NameExpression.FirstName, 9580);
configuration.AddExecuteOrderRule(NameExpression.LastName, 9560);
configuration.AddExecuteOrderRule(NameExpression.Domain, 9550);
configuration.AddExecuteOrderRule(NameExpression.Email, 9540);
configuration.AddExecuteOrderRule(x => x.PropertyType == typeof(string), 2000);
configuration.AddExecuteOrderRule(x => x.PropertyType.IsClass, 1000);
Another example of ordering priority is that the default configuration defines the enum properties will be assigned before other property types because they tend to be reference data that might define how other properties are assigned.
configuration.AddExecuteOrderRule(x => x.PropertyType.IsEnum, 4000);
configuration.AddExecuteOrderRule(x => x.PropertyType.IsValueType, 3000);
configuration.AddExecuteOrderRule(x => x.PropertyType == typeof(string), 2000);
configuration.AddExecuteOrderRule(x => x.PropertyType.IsClass, 1000);
An IIgnoreRule
identifies a property that should not have a value generated for it.
Ignore rules can be added to a IConfigurationModule
so that the configuration is reusable.
public class TestModule : IConfigurationModule
{
/// <inheritdoc />
public void Configure(IBuildConfiguration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration.AddIgnoreRule<Request>(x => x.Data);
}
}
If a specific scenario needs to ignore a property then this can be inlined in the create call.
var model = Model
.Ignoring<PersonalDetails>(x => x.Age)
.Create<CreateAccountRequest>();
An IPostBuildAction
is like the Set
and SetEach
extension methods. It provides the opportunity to tweak an instance after it has been created or populated and are evaluated in descending priority order.
An IPropertyResolver
is assists in resolving the properties on a type when populating instances of a model.
The DefaultPropertyResolver
provides the logic for identifying the properties on a type that can be populated and using IExecuteOrderRule
values to determine the priority order in which they should be populated. It also supports identifying whether a property should be populated even though it can be. The typcial scenario here is that a property that returns a value provided in a constructor parameter should not be overwritten with a new value.
An ITypeCreator
creates instances of class or struct types. There are several type creators that ModelBuilder provides out of the box. These are:
- StructTypeCreator
- EnumerableTypeCreator
- ArrayTypeCreator
- DefaultTypeCreator
StructTypeCreator
provides the logic for creating struct
values that either do not have a constructor defined or define a constructor with parameters.
ArrayTypeCreate
will create instances of Array types and populates the instance with data.
EnumerableTypeCreate
will create instances of most types derived from IEnumerable<T>
and populates the instance with data.
DefaultTypeCreator
will create a new instance of any type using Activator.CreateInstance
and supports the optional provision of constructor arguments. Any parameters required by the constructor will also be created.
A TypeMappingRule
can identify a mapping between types when one needs to be created. For example, a Stream
property on a type can not be created as it is an abstract type. A type mapping can identify the type of Stream
to create however.
This can be defined in a configuration module.
public class TestModule : IConfigurationModule
{
/// <inheritdoc />
public void Configure(IBuildConfiguration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration.AddTypeMappingRule<Stream, MemoryStream>();
}
}
A type mapping could also be inlined.
var model = Model.Mapping<Stream, MemoryStream>().Create<RequestPayload>();
An ITypeResolver
identifies a type to create.
The DefaultTypeResolver
provides some automatic detection of types to create when the type requested is an interfaces or abstract type. This is helpful for testing code that uses interfaces and abstract classes for models and their properties/parameters. It reduces the number of times that a manual type mapping will be required however results may be non-deterministic.
The DefaultTypeResolver
will scan all the types in the assembly that defines the requested type. It will try to create an object using the first matching type it identifies.
For example, you may have a class that returns an interface property that is not already supported by a ITypeCreator
or TypeMappingRule
.
public interface IManifest
{
bool IsCurrent();
DateTimeOffset Sent { get; set; }
DateTimeOffset ExpectedAt { get; set; }
}
public class Manifest : IManifest
{
public bool IsCurrent()
{
var now = DateTimeOffset.UtcNow;
return (Sent < now && ExpectedAt > now);
}
public DateTimeOffset Sent { get; set; }
public DateTimeOffset ExpectedAt { get; set; }
}
public class ShippingContainer
{
public IManifest Manifest { get; set; }
}
The ShippingContainer model here can then be created without an specific type mapping as DefaultTypeResolve
will automatically identify that a Manifest instance should be created when creating an IManifest.
var model = Model.Create<ShippingContainer>();
An IValueGenerator
typically creates values that do not require constructor parameters. There are many value generators that come out of the box. These are:
- AddressValueGenerator
- AgeValueGenerator
- BooleanValueGenerator
- CityValueGenerator
- CompanyValueGenerator
- CountryValueGenerator
- CountValueGenerator
- DateOfBirthValueGenerator
- DateTimeValueGenerator
- DomainNameValueGenerator
- EmailValueGenerator
- EnumValueGenerator
- FirstNameValueGenerator
- GenderValueGenerator
- GuidValueGenerator
- IPAddressValueGenerator
- LastNameValueGenerator
- MailinatorEmailValueGenerator (NOTE: This generator is not included in DefaultBuildConfiguration by default)
- MiddleNameValueGenerator
- NumericValueGenerator
- PhoneValueGenerator
- PostCodeValueGenerator
- StateValueGenerator
- StringValueGenerator
- SuburbValueGenerator
- TimeZoneInfoValueGenerator
- TimeZoneValueGenerator
- UriValueGenerator
Value generators create values either using random data (string, guid, numbers etc) or use embedded resource data for entity type properties (names, addresses etc) to provide pseudo-random values that are appropriate for their purpose.
Some generators also use relative data on the object being created to determine more appropriate values to generate. For example, a Gender parameter or property will restrict the value created for FirstName which will then be used to populate EmailAddress.
This project is supported by JetBrains
The package had some large design changes that introduce breaking changes to the API if you were customising the model creation process. The following changes have been made.
- All ValueGenerator types have been moved into a ValueGenerators namespace.
- All TypeCreator types have been moved into a TypeCreators namespace.
- CreationRule has been replaced with ExpressionCreationRule, ParameterPredicateCreationRule, PropertyPredicateCreationRule, TypePredicateCreationRule and RegexCreationRule which are in the CreationRules namespace.
- The combination of build strategy, compiler and configuration have been replaced with just
IBuildConfiguration
. - IBuildConfiguration is now mutable. A new IBuildConfiguration instance is created for each call to a static method on the
Model
class and that configuration instance is used for that entire creation process. Any mutations to the build configuration will apply until the entire build tree has completed. ICompilerModule
has been renamed toIConfigurationModule
and now configuresIBuildConfiguration
.- The
IgnoreRule
class has been replaced with theIIgnoreRule
interface in the IgnoreRules namespace. The ignore rules provided include ExpressionIgnoreRule, PredicateIgnoreRule and RegexIgnoreRule. The logic for processing rule matches moves fromDefaultPropertyResolver
to the rule itself. - Renamed IValueGenerator.IsSupported to IsMatch
- Renamed IPostBuildAction.IsSupported to IsMatch
- Added IBuildConfiguration.TypeResolver
- Added IBuildConfiguration parameter to ITypeCreator.CanCreate
- ICreationRule, ITypeCreator, IValueGenerator, IPostBuildAction and IBuildLog now have overloads for ParameterInfo, PropertyInfo and Type rather than (Type, string)
- IExecuteOrderRule and IIgnoreRule now have overloads for PropertyInfo rather than (Type, string)
- RelativeValueGenerator no longer supports targeting a specific property as defined by the constructor. Getting a value from another property is now only available with an explicit call to
GetPropertyValue
. - Added caching support to DefaultPropertyResolver and DefaultConstructorResolver