diff --git a/Directory.Build.props b/Directory.Build.props
index 96677dd..82c89e7 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -25,7 +25,7 @@
git
README.md
autopoco;testdata;test-data;poco;generator;fluent;fake-data
- 6.0: Lambda data sources (FuncSource, Setup(...).From(() => ...), Source(expr, () => ...) at generation time). Opt-in nullability-aware null generation via RespectNullableAnnotations(threshold). Generic NumberSource<T> and NullableNumberSource<T> with min/max for every built-in numeric type (integers uniform and inclusive, floating point and decimal overflow safe). Records and immutable types are constructed through their constructor, parameters are fed from the registered members with the same name, no parameterless constructor needed anymore; unconfigured struct parameters stay default instead of throwing. Seeds: UseSeed(seed) per factory, CreateSession(recursionLimit, seed) per session, every member gets its own stream derived from the seed. Own deterministic generator (StableRandom, xoshiro256**) instead of System.Random, sequences are stable across .NET versions. Breaking: AutoPocoGlobalSettings removed (AutoPocoDefaults constants instead), every generated sequence changed once. IDataSource<T> is covariant. Bug fixes: Nullable* sources honor an explicit null creation threshold (null positions of stable sequences changed), DateTime/DateOnly/TimeOnly/DateOfBirth sources cover their whole range including the upper bound (sequences changed), RandomUtfTextSource no longer loops forever. Build and packaging modernized: GitHub Actions CI, trusted publishing to nuget.org, MinVer tag based versioning, central package management, xunit.v3 tests on net8/9/10. FluentAssertions and Moq are no longer dependencies of the library. See README for details.
+ 6.0: Lambda data sources (FuncSource, Setup(...).From(() => ...), Source(expr, () => ...) at generation time). Opt-in nullability-aware null generation via RespectNullableAnnotations(threshold). Generic NumberSource<T> and NullableNumberSource<T> with min/max for every built-in numeric type (integers uniform and inclusive, floating point and decimal overflow safe). Records and immutable types are constructed through their constructor, parameters are fed from the registered members with the same name, no parameterless constructor needed anymore; unconfigured struct parameters stay default instead of throwing. Seeds: UseSeed(seed) per factory, CreateSession(recursionLimit, seed) per session, every member gets its own stream derived from the seed. Own deterministic generator (StableRandom, xoshiro256**) instead of System.Random, sequences are stable across .NET versions. Breaking: AutoPocoGlobalSettings removed (AutoPocoDefaults constants instead), every generated sequence changed once. Index-aware Impose on lists and selections (value from the position, or from position and item), Impose(member, item => value) on a single generator. IDataSource<T> is covariant. Bug fixes: Nullable* sources honor an explicit null creation threshold (null positions of stable sequences changed), DateTime/DateOnly/TimeOnly/DateOfBirth sources cover their whole range including the upper bound (sequences changed), RandomUtfTextSource no longer loops forever. Build and packaging modernized: GitHub Actions CI, trusted publishing to nuget.org, MinVer tag based versioning, central package management, xunit.v3 tests on net8/9/10. FluentAssertions and Moq are no longer dependencies of the library. See README for details.
diff --git a/README.md b/README.md
index afbc9ba..abf2bda 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,23 @@ var other = factory.CreateSession(5, 4711); // seed 4711, same configuratio
* Own random number generator: every source draws from `StableRandom` (xoshiro256** seeded through SplitMix64), a `Random` subclass owned by AutoPoco, instead of `System.Random`. The sequence for a seed is defined by AutoPoco's code alone, so test data stays the same across .NET versions; Microsoft explicitly does not promise that for a seeded `System.Random`. The generator is pinned by tests. Rule from now on: catalogs (names, cities, zip codes, ...) and source algorithms are frozen within a major version, a change to either is a major version bump.
* Breaking: `AutoPocoGlobalSettings` is gone, it was mutable process-wide state that leaked between test fixtures. The defaults are read-only constants in `AutoPocoDefaults` (`Seed`, `NullCreationThreshold`, `RecursionLimit`). Set a seed with `UseSeed`, null thresholds per source or with `RespectNullableAnnotations(threshold)`.
* Breaking: because of the own generator and the per-member seeds, every generated sequence changed once with 6.0. Tests that pin generated values need new expectations, this is the last time.
+* Index-aware `Impose`: on a list or a selection the imposed value can depend on the position of the item (0 based), and on the item as generated so far. The position is the one in the whole list, also inside `First`/`Next` and after `Random`. A single generator gets `Impose(member, item => value)` for values that depend on other members.
+
+```CSHARP
+var users = session.List(100)
+ .Impose(u => u.Id, i => i + 1) // 1..100
+ .Impose(u => u.EmailAddress, (i, u) => $"{u.FirstName}.{u.LastName}{i}@example.test")
+ .First(10).Impose(u => u.City, i => $"Branch {i}")
+ .All()
+ .Get();
+
+var admin = session.Single()
+ .Impose(u => u.LastName, "Ashton")
+ .Impose(u => u.EmailAddress, u => $"{u.FirstName}.{u.LastName}@example.test")
+ .Get();
+```
+
+ The item lambda runs after the configured sources and after every `Impose` registered before it, so it sees those values.
* `IDataSource` is covariant now, so a source of `string` (e.g. `CitySource`) can be used for a `string?` member without a nullability warning.
* Bug fix: every `Nullable*` source ignored an explicit null creation threshold. The fixed array and dictionary based sources (names, companies, cities, capitals, countries, states, zip codes, urls) even used the threshold as their random seed. The threshold is honored now. As a consequence the null positions in the stable sequences of these sources changed, the data itself comes in the same order as before.
* Bug fix: `DateTimeSource`, `DateOnlySource`, `TimeOnlySource` and `DateOfBirthSource` never produced the upper end of their ranges: no December, no 31st, no 23:00, no minute or second 59, and the maximum year of a date of birth was never reached. Ranges within a single year could produce values outside the range. All four sources now pick uniformly from the whole range, both bounds inclusive, and throw an `ArgumentOutOfRangeException` when the maximum lies before the minimum. `TimeOnlySource` supports ranges that wrap around midnight (22:00 to 02:00). `DateTimeSource` keeps the `DateTimeKind` of the minimum date. The generated sequences for a given seed changed.
diff --git a/holonsoft.AutoPoco.Tests/Functionality.Tests/Engine/IndexAwareImposeTests.cs b/holonsoft.AutoPoco.Tests/Functionality.Tests/Engine/IndexAwareImposeTests.cs
new file mode 100644
index 0000000..ec1eae8
--- /dev/null
+++ b/holonsoft.AutoPoco.Tests/Functionality.Tests/Engine/IndexAwareImposeTests.cs
@@ -0,0 +1,116 @@
+using Shouldly;
+using Moq;
+using System.Linq.Expressions;
+using Xunit;
+using holonsoft.AutoPoco.Actions;
+using holonsoft.AutoPoco.Configuration;
+using holonsoft.AutoPoco.Engine;
+using holonsoft.AutoPoco.Engine.Interfaces;
+using holonsoft.AutoPoco.Tests.Common;
+
+namespace holonsoft.AutoPoco.Tests.Functionality.Tests.Engine;
+
+public class IndexAwareImposeTests {
+ private static readonly Expression> _lastName = x => x.LastName;
+
+ private static List>> Mocks(int count)
+ => Enumerable.Range(0, count).Select(_ => new Mock>()).ToList();
+
+ [Fact]
+ public void CollectionImposeWithIndexPassesThePositionOfEveryItem() {
+ var mocks = Mocks(5);
+ var context = new CollectionContext>(mocks.Select(m => m.Object));
+
+ context.Impose(_lastName, i => $"item-{i}").ShouldBeSameAs(context);
+
+ for (var i = 0; i < 5; i++)
+ mocks[i].Verify(m => m.Impose(_lastName, $"item-{i}"), Times.Once());
+ }
+
+ [Fact]
+ public void CollectionImposeWithIndexAndItemRegistersAFactoryPerItem() {
+ var mocks = Mocks(3);
+ var context = new CollectionContext>(mocks.Select(m => m.Object));
+ var captured = new List>();
+ foreach (var mock in mocks)
+ mock.Setup(m => m.Impose(_lastName, It.IsAny>()))
+ .Callback((Expression> _, Func f) => captured.Add(f));
+
+ context.Impose(_lastName, (i, user) => $"{user.FirstName}-{i}");
+
+ captured.Count.ShouldBe(3);
+ var user = new SimpleUser { FirstName = "Rob", LastName = "", EmailAddress = "", Role = new SimpleUserRole { Name = "" }, Properties = [] };
+ captured.Select(f => f(user)).ShouldBe(["Rob-0", "Rob-1", "Rob-2"]);
+ }
+
+ [Fact]
+ public void SelectionImposeWithIndexUsesThePositionInTheWholeCollectionAlsoAfterRandom() {
+ var mocks = Mocks(6);
+ var context = new CollectionContext>(mocks.Select(m => m.Object));
+
+ context.Random(6).Impose(_lastName, i => $"pos-{i}");
+
+ for (var i = 0; i < 6; i++)
+ mocks[i].Verify(m => m.Impose(_lastName, $"pos-{i}"), Times.Once());
+ }
+
+ [Fact]
+ public void SelectionImposeWithIndexOnlyTouchesTheSelection() {
+ var mocks = Mocks(6);
+ var context = new CollectionContext>(mocks.Select(m => m.Object));
+
+ context.First(2).Next(2).Impose(_lastName, i => $"pos-{i}");
+
+ mocks[0].Verify(m => m.Impose(_lastName, It.IsAny()), Times.Never());
+ mocks[2].Verify(m => m.Impose(_lastName, "pos-2"), Times.Once());
+ mocks[3].Verify(m => m.Impose(_lastName, "pos-3"), Times.Once());
+ mocks[5].Verify(m => m.Impose(_lastName, It.IsAny()), Times.Never());
+ }
+
+ [Fact]
+ public void SelectionImposeWithIndexAndItemRegistersAFactory() {
+ var mocks = Mocks(2);
+ var context = new CollectionContext>(mocks.Select(m => m.Object));
+
+ context.First(2).Impose(_lastName, (i, user) => $"{i}");
+
+ mocks[0].Verify(m => m.Impose(_lastName, It.IsAny>()), Times.Once());
+ mocks[1].Verify(m => m.Impose(_lastName, It.IsAny>()), Times.Once());
+ }
+
+ [Fact]
+ public void NullFactoriesAreRejected() {
+ var context = new CollectionContext>(Mocks(1).Select(m => m.Object));
+
+ Should.Throw(() => context.Impose(_lastName, (Func) null!));
+ Should.Throw(() => context.Impose(_lastName, (Func) null!));
+ Should.Throw(() => context.First(1).Impose(_lastName, (Func) null!));
+ Should.Throw(() => context.First(1).Impose(_lastName, (Func) null!));
+ }
+
+ [Fact]
+ public void MemberSetFromFuncActionSetsPropertiesAndFields() {
+ var propertyAction = new ObjectMemberSetFromFuncAction(
+ new EngineTypePropertyMember(typeof(SimpleUserRole).GetProperty(nameof(SimpleUserRole.Name))!), x => x.Name + "p");
+ var fieldAction = new ObjectMemberSetFromFuncAction(
+ new EngineTypeFieldMember(typeof(SimpleFieldClass).GetField(nameof(SimpleFieldClass.SomeField))!), x => x.SomeOtherField + "f");
+ var role = new SimpleUserRole { Name = "role" };
+ var fields = new SimpleFieldClass { SomeOtherField = "other" };
+
+ propertyAction.Enact(null, role);
+ fieldAction.Enact(null, fields);
+
+ role.Name.ShouldBe("rolep");
+ fields.SomeField.ShouldBe("otherf");
+ }
+
+ [Fact]
+ public void MemberSetFromFuncActionRejectsMethodsAndNulls() {
+ var method = new EngineTypeMethodMember(typeof(SimpleUser).GetMethod(nameof(SimpleUser.SetPassword))!);
+
+ Should.Throw(() => new ObjectMemberSetFromFuncAction(method, x => ""));
+ Should.Throw(() => new ObjectMemberSetFromFuncAction(null!, x => ""));
+ Should.Throw(() => new ObjectMemberSetFromFuncAction(
+ new EngineTypePropertyMember(typeof(SimpleUser).GetProperty(nameof(SimpleUser.LastName))!), null!));
+ }
+}
diff --git a/holonsoft.AutoPoco.Tests/Integration.Tests/Complete/WhenImposingByIndex.cs b/holonsoft.AutoPoco.Tests/Integration.Tests/Complete/WhenImposingByIndex.cs
new file mode 100644
index 0000000..c10e7e7
--- /dev/null
+++ b/holonsoft.AutoPoco.Tests/Integration.Tests/Complete/WhenImposingByIndex.cs
@@ -0,0 +1,105 @@
+using Shouldly;
+using Xunit;
+using holonsoft.AutoPoco.DataSources.Business;
+using holonsoft.AutoPoco.Engine.Interfaces;
+using holonsoft.AutoPoco.Extensions;
+using holonsoft.AutoPoco.Tests.Common;
+
+namespace holonsoft.AutoPoco.Tests.Integration.Tests.Complete;
+
+public class WhenImposingByIndex {
+ private static IGenerationSession CreateSession()
+ => AutoPocoContainer.Configure(x => {
+ x.Conventions(c => c.UseDefaultConventions());
+ x.Include()
+ .Setup(c => c.FirstName).Use()
+ .Setup(c => c.LastName).Use();
+ }).CreateSession();
+
+ [Fact]
+ public void TheIndexGivesEveryItemItsOwnValue() {
+ var users = CreateSession().List(10)
+ .Impose(u => u.Id, i => (Int128) (i + 1))
+ .Impose(u => u.City, i => $"City {i}")
+ .Get();
+
+ users.Select(u => u.Id).ShouldBe(Enumerable.Range(1, 10).Select(i => (Int128) i));
+ users.Select(u => u.City).ShouldBe(Enumerable.Range(0, 10).Select(i => $"City {i}"));
+ }
+
+ [Fact]
+ public void TheItemFactorySeesTheGeneratedMembers() {
+ var users = CreateSession().List(5)
+ .Impose(u => u.EmailAddress, (i, u) => $"{u.FirstName}.{u.LastName}.{i}@example.test".ToLowerInvariant())
+ .Get();
+
+ users.ShouldAllBe(u => u.EmailAddress.StartsWith(u.FirstName.ToLowerInvariant() + "."));
+ users.Select(u => u.EmailAddress).ShouldBe(users.Select((u, i) => $"{u.FirstName}.{u.LastName}.{i}@example.test".ToLowerInvariant()));
+ }
+
+ [Fact]
+ public void TheItemFactorySeesEarlierImposedValues() {
+ var users = CreateSession().List(3)
+ .Impose(u => u.FirstName, "Rob")
+ .Impose(u => u.EmailAddress, (i, u) => $"{u.FirstName}{i}@example.test")
+ .Get();
+
+ users.Select(u => u.EmailAddress).ShouldBe(["Rob0@example.test", "Rob1@example.test", "Rob2@example.test"]);
+ }
+
+ [Fact]
+ public void ASelectionUsesThePositionInTheWholeList() {
+ var users = CreateSession().List(10)
+ .First(3).Impose(u => u.City, i => $"first {i}")
+ .Next(3).Impose(u => u.City, (i, u) => $"next {i}")
+ .All()
+ .Get();
+
+ users.Take(3).Select(u => u.City).ShouldBe(["first 0", "first 1", "first 2"]);
+ users.Skip(3).Take(3).Select(u => u.City).ShouldBe(["next 3", "next 4", "next 5"]);
+ users.Skip(6).ShouldAllBe(u => u.City != null && !u.City.StartsWith("first") && !u.City.StartsWith("next"));
+ }
+
+ [Fact]
+ public void ARandomSelectionKeepsTheOriginalPositions() {
+ var users = CreateSession().List(20)
+ .Random(8).Impose(u => u.Id, i => (Int128) i)
+ .Impose(u => u.City, i => "picked")
+ .All()
+ .Get();
+
+ var picked = users.Select((u, i) => (u, i)).Where(t => t.u.City == "picked").ToList();
+ picked.Count.ShouldBe(8);
+ picked.ShouldAllBe(t => t.u.Id == t.i);
+ }
+
+ [Fact]
+ public void ASingleGeneratorCanImposeFromTheObject() {
+ var user = CreateSession().Single()
+ .Impose(u => u.LastName, "Ashton")
+ .Impose(u => u.EmailAddress, u => $"{u.FirstName}.{u.LastName}@example.test")
+ .Get();
+
+ user.EmailAddress.ShouldBe($"{user.FirstName}.Ashton@example.test");
+ }
+
+ [Fact]
+ public void FieldsWorkAsWell() {
+ var items = CreateSession().List(4)
+ .Impose(c => c.SomeField, i => $"field {i}")
+ .Get();
+
+ items.Select(c => c.SomeField).ShouldBe(["field 0", "field 1", "field 2", "field 3"]);
+ }
+
+ [Fact]
+ public void TheClassicImposeStillWorksNextToTheNewOverloads() {
+ var users = CreateSession().List(3)
+ .Impose(u => u.FirstName, "Rob")
+ .Impose(u => u.Id, i => (Int128) i)
+ .Get();
+
+ users.ShouldAllBe(u => u.FirstName == "Rob");
+ users.Select(u => u.Id).ShouldBe([(Int128) 0, 1, 2]);
+ }
+}
diff --git a/holonsoft.AutoPoco/Actions/ObjectMemberSetFromFuncAction.cs b/holonsoft.AutoPoco/Actions/ObjectMemberSetFromFuncAction.cs
new file mode 100644
index 0000000..efba9ae
--- /dev/null
+++ b/holonsoft.AutoPoco/Actions/ObjectMemberSetFromFuncAction.cs
@@ -0,0 +1,36 @@
+using holonsoft.AutoPoco.Configuration;
+using holonsoft.AutoPoco.Engine.Interfaces;
+
+namespace holonsoft.AutoPoco.Actions;
+
+///
+/// Sets a property or field from a lambda that receives the object as generated so far, so the value
+/// can depend on other members (or on the position in a collection, captured by the lambda).
+///
+public class ObjectMemberSetFromFuncAction : IObjectAction {
+ private readonly EngineTypeMember _member;
+ private readonly Func _valueFactory;
+
+ public ObjectMemberSetFromFuncAction(EngineTypeMember member, Func valueFactory) {
+ ArgumentNullException.ThrowIfNull(member);
+ ArgumentNullException.ThrowIfNull(valueFactory);
+ if (member is not (EngineTypePropertyMember or EngineTypeFieldMember))
+ throw new ArgumentException($"Member '{member.Name}' is neither a property nor a field.", nameof(member));
+
+ _member = member;
+ _valueFactory = valueFactory;
+ }
+
+ public void Enact(IGenerationContext? context, object target) {
+ var value = _valueFactory((TPoco) target);
+
+ switch (_member) {
+ case EngineTypePropertyMember property:
+ property.PropertyInfo.SetValue(target, value, null);
+ break;
+ case EngineTypeFieldMember field:
+ field.FieldInfo.SetValue(target, value);
+ break;
+ }
+ }
+}
diff --git a/holonsoft.AutoPoco/Engine/CollectionContext.cs b/holonsoft.AutoPoco/Engine/CollectionContext.cs
index cdf3262..2adfafe 100644
--- a/holonsoft.AutoPoco/Engine/CollectionContext.cs
+++ b/holonsoft.AutoPoco/Engine/CollectionContext.cs
@@ -5,57 +5,82 @@
namespace holonsoft.AutoPoco.Engine;
-/// one generator per element
+///
+/// A generator with its position in the collection it belongs to. The position survives Random.
+///
+public sealed record IndexedGenerator(int Index, IObjectGenerator Generator);
+
+/// one generator per element, in collection order
/// seed of the shuffle behind , the session seed when created by a session
-public class CollectionContext(IEnumerable> generators, int seed = AutoPocoDefaults.Seed)
- : ICollectionContext where TCollection : ICollection {
- private readonly Random _random = new StableRandom(seed);
-
- public ICollectionContext Impose(Expression> propertyExpr,
- TMember value) {
- foreach (var item in generators)
- item.Impose(propertyExpr, value);
+public class CollectionContext : ICollectionContext where TCollection : ICollection {
+ private readonly IndexedGenerator[] _generators;
+ private readonly Random _random;
+
+ public CollectionContext(IEnumerable> generators, int seed = AutoPocoDefaults.Seed) {
+ ArgumentNullException.ThrowIfNull(generators);
+ _generators = generators.Select((g, i) => new IndexedGenerator(i, g)).ToArray();
+ _random = new StableRandom(seed);
+ }
+
+ public ICollectionContext Impose(Expression> propertyExpr, TMember value) {
+ foreach (var item in _generators)
+ item.Generator.Impose(propertyExpr, value);
+ return this;
+ }
+
+ public ICollectionContext Impose(Expression> propertyExpr, Func valueFactory) {
+ ArgumentNullException.ThrowIfNull(valueFactory);
+ foreach (var item in _generators)
+ item.Generator.Impose(propertyExpr, valueFactory(item.Index));
+ return this;
+ }
+
+ public ICollectionContext Impose(Expression> propertyExpr, Func valueFactory) {
+ ArgumentNullException.ThrowIfNull(valueFactory);
+ foreach (var item in _generators) {
+ var index = item.Index;
+ item.Generator.Impose(propertyExpr, poco => valueFactory(index, poco));
+ }
+
return this;
}
public ICollectionContext Source(Expression> propertyExpr,
IDataSource dataSource) {
- foreach (var item in generators)
- item.Source(propertyExpr, dataSource);
+ foreach (var item in _generators)
+ item.Generator.Source(propertyExpr, dataSource);
return this;
}
- public ICollectionSequenceSelectionContext First(int count) => new CollectionSequenceSelectionContext(
- this,
- generators,
- count);
+ public ICollectionSequenceSelectionContext First(int count)
+ => new CollectionSequenceSelectionContext(this, _generators, count);
public ICollectionSequenceSelectionContext Random(int count)
- // Randomize and return
+ // Randomize and return, the original positions travel with the generators
=> new CollectionSequenceSelectionContext(
this,
- generators.OrderBy(r => _random.Next()).ToArray(),
+ _generators.OrderBy(r => _random.Next()).ToArray(),
count);
public TCollection Get() {
// Create an array if it's an array
if (typeof(TPoco[]).IsAssignableFrom(typeof(TCollection)))
- return (TCollection) (object) generators.Select(x => x.Get()).ToArray();
+ return (TCollection) (object) _generators.Select(x => x.Generator.Get()).ToArray();
// Return a list if it's a list
if (typeof(IList<>).MakeGenericType(typeof(TPoco)).IsAssignableFrom(typeof(TCollection)))
- return (TCollection) (object) generators.Select(x => x.Get()).ToList();
+ return (TCollection) (object) _generators.Select(x => x.Generator.Get()).ToList();
throw new InvalidOperationException();
}
public ICollectionContext Invoke(Expression> methodExpr) {
- foreach (var item in generators)
- item.Invoke(methodExpr);
+ foreach (var item in _generators)
+ item.Generator.Invoke(methodExpr);
return this;
}
public ICollectionContext Invoke(Expression> methodExpr) {
- foreach (var item in generators)
- item.Invoke(methodExpr);
+ foreach (var item in _generators)
+ item.Generator.Invoke(methodExpr);
return this;
}
-}
\ No newline at end of file
+}
diff --git a/holonsoft.AutoPoco/Engine/CollectionSequenceSelectionContext.cs b/holonsoft.AutoPoco/Engine/CollectionSequenceSelectionContext.cs
index 66c6023..00beba8 100644
--- a/holonsoft.AutoPoco/Engine/CollectionSequenceSelectionContext.cs
+++ b/holonsoft.AutoPoco/Engine/CollectionSequenceSelectionContext.cs
@@ -5,16 +5,18 @@ namespace holonsoft.AutoPoco.Engine;
public class CollectionSequenceSelectionContext
: ICollectionSequenceSelectionContext where TCollection : ICollection {
- private readonly IEnumerable> _allGenerators;
+ private readonly IReadOnlyList> _allGenerators;
private int _currentCount;
private int _currentSkip;
private readonly ICollectionContext _parentContext;
- private IEnumerable>? _selected = null;
+ private IReadOnlyList> _selected = [];
public CollectionSequenceSelectionContext(
ICollectionContext parentContext,
- IEnumerable> generators,
+ IReadOnlyList> generators,
int initialPull) {
+ ArgumentNullException.ThrowIfNull(parentContext);
+ ArgumentNullException.ThrowIfNull(generators);
_allGenerators = generators;
_currentCount = 0;
_currentSkip = 0;
@@ -22,35 +24,53 @@ public CollectionSequenceSelectionContext(
Next(initialPull);
}
- public int Remaining => _allGenerators.Count() - (_currentSkip + _currentCount);
+ ///
+ /// Selection over generators without positions, they get their position from the enumeration order.
+ ///
+ public CollectionSequenceSelectionContext(
+ ICollectionContext parentContext,
+ IEnumerable> generators,
+ int initialPull)
+ : this(parentContext, (generators ?? throw new ArgumentNullException(nameof(generators))).Select((g, i) => new IndexedGenerator(i, g)).ToList(), initialPull) { }
+
+ public int Remaining => _allGenerators.Count - (_currentSkip + _currentCount);
public ICollectionSequenceSelectionContext Impose(
Expression> propertyExpr, TMember value) {
+ foreach (var item in _selected)
+ item.Generator.Impose(propertyExpr, value);
+ return this;
+ }
- if (_selected == null)
- throw new Exception(@"Impose has no values for _selected!");
-
+ public ICollectionSequenceSelectionContext Impose(
+ Expression> propertyExpr, Func valueFactory) {
+ ArgumentNullException.ThrowIfNull(valueFactory);
foreach (var item in _selected)
- item.Impose(propertyExpr, value);
+ item.Generator.Impose(propertyExpr, valueFactory(item.Index));
return this;
}
- public ICollectionSequenceSelectionContext Invoke(Expression> methodExpr) {
- if (_selected == null)
- throw new Exception(@"Invoke(Expression>) has no values for _selected!");
+ public ICollectionSequenceSelectionContext Impose(
+ Expression> propertyExpr, Func valueFactory) {
+ ArgumentNullException.ThrowIfNull(valueFactory);
+ foreach (var item in _selected) {
+ var index = item.Index;
+ item.Generator.Impose(propertyExpr, poco => valueFactory(index, poco));
+ }
+
+ return this;
+ }
+ public ICollectionSequenceSelectionContext Invoke(Expression> methodExpr) {
foreach (var item in _selected)
- item.Invoke(methodExpr);
+ item.Generator.Invoke(methodExpr);
return this;
}
public ICollectionSequenceSelectionContext Invoke(
Expression> methodExpr) {
- if (_selected == null)
- throw new Exception(@"Invoke has no values for _selected!");
-
foreach (var item in _selected)
- item.Invoke(methodExpr);
+ item.Generator.Invoke(methodExpr);
return this;
}
@@ -60,11 +80,12 @@ public ICollectionSequenceSelectionContext Next(int count) {
_currentCount = count;
_selected = _allGenerators
.Skip(_currentSkip)
- .Take(_currentCount);
+ .Take(_currentCount)
+ .ToList();
return this;
}
- public ICollectionContext All()
+ public ICollectionContext All()
=> _parentContext;
-}
\ No newline at end of file
+}
diff --git a/holonsoft.AutoPoco/Engine/Interfaces/ICollectionContext.cs b/holonsoft.AutoPoco/Engine/Interfaces/ICollectionContext.cs
index 7094954..405ca5c 100644
--- a/holonsoft.AutoPoco/Engine/Interfaces/ICollectionContext.cs
+++ b/holonsoft.AutoPoco/Engine/Interfaces/ICollectionContext.cs
@@ -8,6 +8,17 @@ public interface ICollectionContext where TCollection : ICol
///
ICollectionContext Impose(Expression> propertyExpr, TMember value);
+ ///
+ /// Imposes a value computed from the position of the item in the collection (0 based).
+ ///
+ ICollectionContext Impose(Expression> propertyExpr, Func valueFactory);
+
+ ///
+ /// Imposes a value computed from the position of the item in the collection (0 based) and the item
+ /// as generated so far, so the value can depend on other members.
+ ///
+ ICollectionContext Impose(Expression> propertyExpr, Func valueFactory);
+
///
/// Overrides the data source for this particular generation scope
///
diff --git a/holonsoft.AutoPoco/Engine/Interfaces/ICollectionSequenceSelectionContext.cs b/holonsoft.AutoPoco/Engine/Interfaces/ICollectionSequenceSelectionContext.cs
index 40564be..898a3cc 100644
--- a/holonsoft.AutoPoco/Engine/Interfaces/ICollectionSequenceSelectionContext.cs
+++ b/holonsoft.AutoPoco/Engine/Interfaces/ICollectionSequenceSelectionContext.cs
@@ -14,6 +14,20 @@ public interface ICollectionSequenceSelectionContext where T
ICollectionSequenceSelectionContext Impose(Expression> propertyExpr,
TMember value);
+ ///
+ /// Imposes a value computed from the position of the item in the whole collection (0 based),
+ /// also after Random.
+ ///
+ ICollectionSequenceSelectionContext Impose(Expression> propertyExpr,
+ Func valueFactory);
+
+ ///
+ /// Imposes a value computed from the position of the item in the whole collection (0 based) and the item
+ /// as generated so far.
+ ///
+ ICollectionSequenceSelectionContext Impose(Expression> propertyExpr,
+ Func valueFactory);
+
///
/// Invokes a method on all the items in the current selection
///
diff --git a/holonsoft.AutoPoco/Engine/Interfaces/IObjectGenerator.cs b/holonsoft.AutoPoco/Engine/Interfaces/IObjectGenerator.cs
index f13c79d..965fd3e 100644
--- a/holonsoft.AutoPoco/Engine/Interfaces/IObjectGenerator.cs
+++ b/holonsoft.AutoPoco/Engine/Interfaces/IObjectGenerator.cs
@@ -18,6 +18,12 @@ public interface IObjectGenerator {
///
IObjectGenerator Impose(Expression> propertyExpr, TMember value);
+ ///
+ /// Imposes a value computed from the object as generated so far: every member configured or imposed
+ /// before is already set when the lambda runs.
+ ///
+ IObjectGenerator Impose(Expression> propertyExpr, Func valueFactory);
+
///
/// Invokes a method on all the items in the current selection
///
diff --git a/holonsoft.AutoPoco/Engine/ObjectGenerator.cs b/holonsoft.AutoPoco/Engine/ObjectGenerator.cs
index d3b83b6..1754a97 100644
--- a/holonsoft.AutoPoco/Engine/ObjectGenerator.cs
+++ b/holonsoft.AutoPoco/Engine/ObjectGenerator.cs
@@ -33,6 +33,13 @@ public IObjectGenerator Impose(Expression> property
return this;
}
+ public IObjectGenerator Impose(Expression> propertyExpr, Func valueFactory) {
+ ArgumentNullException.ThrowIfNull(valueFactory);
+ var member = ReflectionHelper.GetMember(propertyExpr);
+ AddAction(new ObjectMemberSetFromFuncAction(member, valueFactory));
+ return this;
+ }
+
public IObjectGenerator Source(Expression> propertyExpr, IDataSource dataSource) {
ArgumentNullException.ThrowIfNull(dataSource);
var member = ReflectionHelper.GetMember(propertyExpr);