Support for instantiation from constructors with parameters
- Dominant language
- C#
- Stars
- 9.7k
- Forks
- 538
- PR merge metrics
- No merged PRs in 30d
Description
### Description
Hello,
My application heavily relies on `record` types for various purposes. Unfortunately, Bogus requires a parameterless constructor to generate entities.
I’ve read about the `CustomInstantiator`, but I’m not a fan of that approach since I’d prefer to keep things consistent across all my tests and use `RuleFor` just like I do with my other classes.
So I’ve come up with a potential solution that probably still needs some refinement. For now, it uses an abstract class that inherits from `Faker`, but it could likely be refactored into an extension method if needed.
### LINQPad Code Example
```csharp
internal abstract class RecordFaker : Faker where TRecord : class
{
///
/// This list represents the parameters to generate
///
private List>? _instantiationPopulateActions;
protected RecordFaker()
{
// We require the strict mode to ensure we are able to call the constructor.
StrictMode(true);
UseConstructorParameterRules();
}
///
/// Replaces the default instantiation behavior with a constructor parameter based one.
/// This implementation uses the rules to generate the parameters. Because this method is designed for records
/// it is safe to assume the parameters will have the same name as properties.
///
private void UseConstructorParameterRules()
{
CreateActions[Default] = faker =>
{
// Discovers the parameters only on the Faker's first run.
// Cached for Generate(int count).
_instantiationPopulateActions ??= RetrievePopulateActions();
object?[] parameterValues = new object?[_instantiationPopulateActions.Count];
for (int i = 0; i < _instantiationPopulateActions.Count; i++)
{
parameterValues[i] = _instantiationPopulateActions[i].Action(faker, default!);
}
return (TRecord)Activator.CreateInstance(typeof(TRecord), parameterValues)!;
};
}
///
/// Retrieves the parameters from the primary constructor
///
///
/// Raised when the method is not able to retrieve the parameter name.
/// Raised when the rule to generate the parameter is missing.
private List> RetrievePopulateActions()
{
string ruleSets = Default;
ParameterInfo[] constructorParameters = typeof(TRecord).GetConstructors().Single().GetParameters();
List> actions = [];
if (Actions.TryGetValue(ruleSets, out Dictionary>? populateActions))
{
foreach (ParameterInfo constructorParameter in constructorParameters)
{
if (constructorParameter.Name is null)
{
throw new NullReferenceException($"Unable to retrieve the parameter name: {constructorParameter.ParameterType}");
}
actions.Add(populateActions[constructorParameter.Name]
?? throw new InvalidOperationException($"Missing rule for the parameter {constructorParameter.Name}"));
}
}
return actions;
}
}
internal sealed record FakeRecord(string Name);
internal sealed class FakeRecordFaker : RecordFaker
{
public FakeRecordFaker()
{
RuleFor(fr => fr.Name, faker => faker.Random.AlphaNumeric(5));
}
}
internal sealed class RecordFakerTests : TestBase
{
[Test]
public void Should_CreateRecord()
{
FakeRecordFaker faker = new();
FakeRecord record = faker.Generate();
Assert.That(record, Is.Not.Null);
Assert.That(record.Name, Is.Not.Empty);
}
}
```
### What alternatives have you considered?
I've read the solution from #460 about passing parameters and tried to extrapolate from that. I was not happy with the result.
### Could you help with a pull-request?
Yes
Contributor guide
Research direction
Start by reviewing the parameter-passing solution in #460 and the LINQPad RecordFaker example, including FakeRecordFaker and RecordFakerTests. Determine how parameterized constructors should work with RuleFor and Generate, then verify that the FakeRecord example can be generated without a parameterless constructor.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- testing-qa
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100