Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1964,6 +1964,14 @@ namespace DynamicData
where TDestination : notnull
where TSource : notnull
where TKey : notnull { }
public static System.IObservable<DynamicData.IChangeSet<TDestination, TKey>> TransformAsync<TDestination, TSource, TKey>(this System.IObservable<DynamicData.IChangeSet<TSource, TKey>> source, System.Func<TSource, DynamicData.Kernel.Optional<TSource>, TKey, System.Threading.CancellationToken, System.Threading.Tasks.Task<TDestination>> transformFactory, DynamicData.TransformAsyncOptions options)
where TDestination : notnull
where TSource : notnull
where TKey : notnull { }
public static System.IObservable<DynamicData.IChangeSet<TDestination, TKey>> TransformAsync<TDestination, TSource, TKey>(this System.IObservable<DynamicData.IChangeSet<TSource, TKey>> source, System.Func<TSource, DynamicData.Kernel.Optional<TSource>, TKey, System.Threading.CancellationToken, System.Threading.Tasks.Task<TDestination>> transformFactory, System.IObservable<System.Func<TSource, TKey, bool>>? forceTransform = null)
where TDestination : notnull
where TSource : notnull
where TKey : notnull { }
public static System.IObservable<DynamicData.IChangeSet<TDestination, TKey>> TransformImmutable<TDestination, TSource, TKey>(this System.IObservable<DynamicData.IChangeSet<TSource, TKey>> source, System.Func<TSource, TDestination> transformFactory)
where TDestination : notnull
where TSource : notnull
Expand Down Expand Up @@ -2108,6 +2116,14 @@ namespace DynamicData
where TDestination : notnull
where TSource : notnull
where TKey : notnull { }
public static System.IObservable<DynamicData.IChangeSet<TDestination, TKey>> TransformSafeAsync<TDestination, TSource, TKey>(this System.IObservable<DynamicData.IChangeSet<TSource, TKey>> source, System.Func<TSource, DynamicData.Kernel.Optional<TSource>, TKey, System.Threading.CancellationToken, System.Threading.Tasks.Task<TDestination>> transformFactory, System.Action<DynamicData.Kernel.Error<TSource, TKey>> errorHandler, DynamicData.TransformAsyncOptions options)
where TDestination : notnull
where TSource : notnull
where TKey : notnull { }
public static System.IObservable<DynamicData.IChangeSet<TDestination, TKey>> TransformSafeAsync<TDestination, TSource, TKey>(this System.IObservable<DynamicData.IChangeSet<TSource, TKey>> source, System.Func<TSource, DynamicData.Kernel.Optional<TSource>, TKey, System.Threading.CancellationToken, System.Threading.Tasks.Task<TDestination>> transformFactory, System.Action<DynamicData.Kernel.Error<TSource, TKey>> errorHandler, System.IObservable<System.Func<TSource, TKey, bool>>? forceTransform = null)
where TDestination : notnull
where TSource : notnull
where TKey : notnull { }
public static System.IObservable<DynamicData.IChangeSet<DynamicData.Node<TObject, TKey>, TKey>> TransformToTree<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, TKey> pivotOn, System.IObservable<System.Func<DynamicData.Node<TObject, TKey>, bool>>? predicateChanged = null)
where TObject : class
where TKey : notnull { }
Expand Down Expand Up @@ -2481,6 +2497,9 @@ namespace DynamicData
public static System.IObservable<DynamicData.IChangeSet<TDestination>> TransformAsync<TSource, TDestination>(this System.IObservable<DynamicData.IChangeSet<TSource>> source, System.Func<TSource, DynamicData.Kernel.Optional<TDestination>, int, System.Threading.Tasks.Task<TDestination>> transformFactory, bool transformOnRefresh = false)
where TSource : notnull
where TDestination : notnull { }
public static System.IObservable<DynamicData.IChangeSet<TDestination>> TransformAsync<TSource, TDestination>(this System.IObservable<DynamicData.IChangeSet<TSource>> source, System.Func<TSource, DynamicData.Kernel.Optional<TDestination>, int, System.Threading.CancellationToken, System.Threading.Tasks.Task<TDestination>> transformFactory, bool transformOnRefresh = false)
where TSource : notnull
where TDestination : notnull { }
public static System.IObservable<DynamicData.IChangeSet<TDestination>> TransformMany<TDestination, TSource>(this System.IObservable<DynamicData.IChangeSet<TSource>> source, System.Func<TSource, DynamicData.IObservableList<TDestination>> manySelector, System.Collections.Generic.IEqualityComparer<TDestination>? equalityComparer = null)
where TDestination : notnull
where TSource : notnull { }
Expand Down Expand Up @@ -3100,4 +3119,4 @@ namespace DynamicData.Tests
public void Dispose() { }
protected virtual void Dispose(bool isDisposing) { }
}
}
}
29 changes: 25 additions & 4 deletions src/DynamicData.Tests/Cache/TransformAsyncFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public void Update()
}




[Theory, InlineData(true), InlineData(false)]
public void TransformOnRefresh(bool transformOnRefresh)
Expand All @@ -215,7 +215,7 @@ public void TransformOnRefresh(bool transformOnRefresh)

results.Data.Count.Should().Be(1);
results.Data.Lookup("SomeOne").Value.AgeGroup.Should().Be("Child");

person.Age = 21;


Expand All @@ -224,15 +224,36 @@ public void TransformOnRefresh(bool transformOnRefresh)

}


[Fact]
public void TransformAsyncCancelsTokenOnUnSubscribe()
{
using var source = new SourceCache<Person, string>(p => p.Name);
var tcs = new TaskCompletionSource<Person>();
using var sub = source.Connect()
.TransformAsync(async (c, p, key, cancel) =>
{
using (cancel.Register(() => tcs.SetCanceled(), useSynchronizationContext: false))
{
return await tcs.Task.ConfigureAwait(false);
}
})
.Subscribe();

source.AddOrUpdate(new Person());

sub.Dispose();
Assert.True(tcs.Task.IsCanceled);
}


[Theory, InlineData(10), InlineData(100)]

public async Task WithMaxConcurrency(int maxConcurrency)
{
/* We need to test whether the max concurrency has any effect.

If maxConcurrency == 100, this test takes a little more than 100 ms
If maxConcurrency = 10, this test takes a little more than 1s
If maxConcurrency = 10, this test takes a little more than 1s

So it works, but how can it be tested in a scientific way ??
*/
Expand Down
25 changes: 24 additions & 1 deletion src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,37 @@ public void TransformOnRefresh(bool transformOnRefresh)
}


[Fact]
public void TransformSafeAsyncCancelsTokenOnUnSubscribe()
{
using var source = new SourceCache<Person, string>(p => p.Name);
var tcs = new TaskCompletionSource<Person>();
using var sub = source.Connect()
.TransformSafeAsync(async (c, p, key, cancel) =>
{
using (cancel.Register(() => tcs.SetCanceled(), useSynchronizationContext: false))
{
return await tcs.Task.ConfigureAwait(false);
}
},
error => Assert.Fail($"Unexpected error: {error}")) // NOTE: Cancellation exception should not be called because the handler should be torn down with subscription
.Subscribe();

source.AddOrUpdate(new Person());

sub.Dispose();
Assert.True(tcs.Task.IsCanceled);
}


[Theory, InlineData(10), InlineData(100)]

public async Task WithMaxConcurrency(int maxConcurrency)
{
/* We need to test whether the max concurrency has any effect.

If maxConcurrency == 100, this test takes a little more than 100 ms
If maxConcurrency = 10, this test takes a little more than 1s
If maxConcurrency = 10, this test takes a little more than 1s

So it works, but how can it be tested in a scientific way ??
*/
Expand Down
24 changes: 23 additions & 1 deletion src/DynamicData.Tests/List/TransformAsyncFixture.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using DynamicData.Kernel;
using DynamicData.Tests.Domain;
using FluentAssertions;
using Xunit;
Expand Down Expand Up @@ -165,4 +166,25 @@ private void TransformOnRefresh()

results.Messages.Last().First().Reason.Should().Be(ListChangeReason.Replace);
}

[Fact]
public void TransformAsyncCancelsTokenOnUnSubscribe()
{
using var source = new SourceList<Person>();
var tcs = new TaskCompletionSource<Person>();
using var sub = source.Connect()
.TransformAsync<Person, Person>(async (c, p, count, cancel) =>
{
using (cancel.Register(() => tcs.SetCanceled(), useSynchronizationContext: false))
{
return await tcs.Task.ConfigureAwait(false);
}
})
.Subscribe();

source.Add(new Person());

sub.Dispose();
Assert.True(tcs.Task.IsCanceled);
}
}
10 changes: 5 additions & 5 deletions src/DynamicData/Cache/Internal/TransformAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace DynamicData.Cache.Internal;

internal class TransformAsync<TDestination, TSource, TKey>(
IObservable<IChangeSet<TSource, TKey>> source,
Func<TSource, Optional<TSource>, TKey, Task<TDestination>> transformFactory,
Func<TSource, Optional<TSource>, TKey, CancellationToken, Task<TDestination>> transformFactory,
Action<Error<TSource, TKey>>? exceptionCallback,
IObservable<Func<TSource, TKey, bool>>? forceTransform = null,
int? maximumConcurrency = null,
Expand Down Expand Up @@ -45,7 +45,7 @@ private IObservable<IChangeSet<TDestination, TKey>> DoTransform(ChangeAwareCache
var toTransform = cache.KeyValues.Where(kvp => shouldTransform(kvp.Value.Source, kvp.Key)).Select(kvp =>
new Change<TSource, TKey>(ChangeReason.Update, kvp.Key, kvp.Value.Source, kvp.Value.Source)).ToArray();

return toTransform.Select(change => Observable.Defer(() => Transform(change).ToObservable()))
return toTransform.Select(change => Observable.FromAsync(t => Transform(change, t)))
.Merge(maximumConcurrency ?? int.MaxValue)
.ToArray()
.Select(transformed => ProcessUpdates(cache, transformed));
Expand All @@ -54,7 +54,7 @@ private IObservable<IChangeSet<TDestination, TKey>> DoTransform(ChangeAwareCache
private IObservable<IChangeSet<TDestination, TKey>> DoTransform(
ChangeAwareCache<TransformedItemContainer, TKey> cache, IChangeSet<TSource, TKey> changes)
{
return changes.Select(change => Observable.FromAsync(() => Transform(change)))
return changes.Select(change => Observable.FromAsync(t => Transform(change, t)))
.Merge(maximumConcurrency ?? int.MaxValue)
.ToArray()
.Select(transformed => ProcessUpdates(cache, transformed));
Expand Down Expand Up @@ -105,13 +105,13 @@ private ChangeSet<TDestination, TKey> ProcessUpdates(ChangeAwareCache<Transforme
return new ChangeSet<TDestination, TKey>(transformed);
}

private async Task<TransformResult> Transform(Change<TSource, TKey> change)
private async Task<TransformResult> Transform(Change<TSource, TKey> change, CancellationToken cancellationToken)
{
try
{
if (change.Reason is ChangeReason.Add or ChangeReason.Update || (change.Reason is ChangeReason.Refresh && transformOnRefresh))
{
var destination = await transformFactory(change.Current, change.Previous, change.Key)
var destination = await transformFactory(change.Current, change.Previous, change.Key, cancellationToken)
.ConfigureAwait(false);
return new TransformResult(change, new TransformedItemContainer(change.Current, destination));
}
Expand Down
32 changes: 30 additions & 2 deletions src/DynamicData/Cache/ObservableCacheEx.TransformAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ public static IObservable<IChangeSet<TDestination, TKey>> TransformAsync<TDestin
return source.TransformAsync((current, _, key) => transformFactory(current, key), forceTransform);
}

/// <inheritdoc cref="TransformAsync{TDestination, TSource, TKey}(IObservable{IChangeSet{TSource, TKey}}, Func{TSource, Optional{TSource}, TKey, Task{TDestination}}, IObservable{Func{TSource, TKey, bool}}?)"/>
/// <remarks>This overload takes a factory that receives the current item the previous item and key.</remarks>
[SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")]
public static IObservable<IChangeSet<TDestination, TKey>> TransformAsync<TDestination, TSource, TKey>(this IObservable<IChangeSet<TSource, TKey>> source, Func<TSource, Optional<TSource>, TKey, Task<TDestination>> transformFactory, IObservable<Func<TSource, TKey, bool>>? forceTransform = null)
where TDestination : notnull
where TSource : notnull
where TKey : notnull
{
source.ThrowArgumentNullExceptionIfNull(nameof(source));
transformFactory.ThrowArgumentNullExceptionIfNull(nameof(transformFactory));

return source.TransformAsync((current, previous, key, _) => transformFactory(current, previous, key), forceTransform);
}

/// <summary>
/// Async version of <see cref="Transform{TDestination, TSource, TKey}(IObservable{IChangeSet{TSource, TKey}}, Func{TSource, Optional{TSource}, TKey, TDestination}, IObservable{Func{TSource, TKey, bool}}?)"/>.
/// Projects each item using an async factory that returns <see cref="Task{TResult}"/>.
Expand Down Expand Up @@ -88,7 +102,7 @@ public static IObservable<IChangeSet<TDestination, TKey>> TransformAsync<TDestin
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="source"/> or <paramref name="transformFactory"/> is <see langword="null"/>.</exception>
[SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")]
public static IObservable<IChangeSet<TDestination, TKey>> TransformAsync<TDestination, TSource, TKey>(this IObservable<IChangeSet<TSource, TKey>> source, Func<TSource, Optional<TSource>, TKey, Task<TDestination>> transformFactory, IObservable<Func<TSource, TKey, bool>>? forceTransform = null)
public static IObservable<IChangeSet<TDestination, TKey>> TransformAsync<TDestination, TSource, TKey>(this IObservable<IChangeSet<TSource, TKey>> source, Func<TSource, Optional<TSource>, TKey, CancellationToken, Task<TDestination>> transformFactory, IObservable<Func<TSource, TKey, bool>>? forceTransform = null)
where TDestination : notnull
where TSource : notnull
where TKey : notnull
Expand Down Expand Up @@ -124,7 +138,7 @@ public static IObservable<IChangeSet<TDestination, TKey>> TransformAsync<TDestin
source.ThrowArgumentNullExceptionIfNull(nameof(source));
transformFactory.ThrowArgumentNullExceptionIfNull(nameof(transformFactory));

return source.TransformAsync((current, _, key) => transformFactory(current, key), options);
return TransformAsync(source, (current, _, key, _) => transformFactory(current, key), options);
}

/// <inheritdoc cref="TransformAsync{TDestination, TSource, TKey}(IObservable{IChangeSet{TSource, TKey}}, Func{TSource, Optional{TSource}, TKey, Task{TDestination}}, IObservable{Func{TSource, TKey, bool}}?)"/>
Expand All @@ -138,6 +152,20 @@ public static IObservable<IChangeSet<TDestination, TKey>> TransformAsync<TDestin
source.ThrowArgumentNullExceptionIfNull(nameof(source));
transformFactory.ThrowArgumentNullExceptionIfNull(nameof(transformFactory));

return TransformAsync(source, (current, previous, key, _) => transformFactory(current, previous, key), options);
}

/// <inheritdoc cref="TransformAsync{TDestination, TSource, TKey}(IObservable{IChangeSet{TSource, TKey}}, Func{TSource, Optional{TSource}, TKey, CancellationToken, Task{TDestination}}, IObservable{Func{TSource, TKey, bool}}?)"/>
/// <remarks>This overload accepts <see cref="TransformAsyncOptions"/> to control concurrency and Refresh handling.</remarks>
[SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")]
public static IObservable<IChangeSet<TDestination, TKey>> TransformAsync<TDestination, TSource, TKey>(this IObservable<IChangeSet<TSource, TKey>> source, Func<TSource, Optional<TSource>, TKey, CancellationToken, Task<TDestination>> transformFactory, TransformAsyncOptions options)
where TDestination : notnull
where TSource : notnull
where TKey : notnull
{
source.ThrowArgumentNullExceptionIfNull(nameof(source));
transformFactory.ThrowArgumentNullExceptionIfNull(nameof(transformFactory));

return new TransformAsync<TDestination, TSource, TKey>(source, transformFactory, null, null, options.MaximumConcurrency, options.TransformOnRefresh).Run();
}
}
Loading
Loading