First re-check the domain for StepsProgression: If we ask a domain expert about our process' steps, I think we get an acceptable answer. Then we can say this is part of our domain.
Next, as you say is this entity or VO? Yes it looks like value object, and actually is a value object as you dont care about the identity of the whole data set, it does not have a unique meaning (if I did not understand domaing wrong).
Assuming you have a UI like a form wizard and every step's inputs are different, I would Implement like this:
public class StepsProgression
{
private readonly string _userId;
public Step1 step1 { get; set; }
public Step2 step2 { get; set; }
public Step3 step3 { get; set; }
public StepsProgression(string userId)
{
_userId = userId;
}
}
// Immutable Step Object
public class Step1
{
public string input1 { get; }
public string input2 { get; }
public string input3 { get; }
}
You instantiate StepsProgression in the first step, so I assume you only know whose steps are they. So constructor only sets user ID. Then when user fills step1 inputs and clicks Next then you create a new (immutable) Step object and set it to step1. If user clicks Back, changes anything from Step1 and clicks Next again, then you again create a new Step1 object and assign it to StepsProgression.
You can think more generic implementation (Step) but as the inputs are all different between steps, this explicit way gives you coding convention.
Edited Implementation:
I understand that StepsProgressions contains a dynamic set of steps which you determine just before creating StepsProgression. Then assuming you know stepTypes beforehand I would change my implementation as follows:
public class StepsProgression
{
private readonly string _userId;
private readonly IStepProgressionBuilder _builder = new StepProgressionBuilder();
public IEnumerable<IStep> Steps { get; set; }
public StepsProgression(string userId, IEnumerable<string> stepTypes)
{
_userId = userId;
foreach (var step in stepTypes) // foreach smells here but you got the point
{
_builder.AddConcreteStep(step) // step = "Step1" for instance.
}
Steps = _builder.Build();
}
}
// Immutable Step Object
public class Step1 : IStep
{
public string input1 { get; }
public string input2 { get; }
public string input3 { get; }
}
// Builder Pattern with Fluent Methods
public interface IStepProgressionBuilder
{
// stepType = "step1" for instance. You have concrete Step1 class, so you return it.
// Start with a switch, then you refactor. (May involve some reflection)
void AddConcreteStep(string stepType);
IEnumerable<IStep> Build();
}
Again you can use one generic Step class, but then your builder should shomehow build a Step by taking each data that Step should contain. This then becomes a codesmell if your steps may have more than 3 properties. Thats why I prefer having concrete Step classes around so that I can build it as a whole dataset.