I'm trying to convert an existing event-based API to a Reactive Observable API. The concrete API I'm working with is the NSNetServiceBrowser in Xamarin.iOS. This API let you browse for network devices using Zeroconf/Bonjour. However, the question would apply to any API of this kind.
The NsNetServiceBrowser offers various events of interest:
- FoundService
- NotSearched
- ServiceRemoved
The FoundService event is raised when a service is discovered, and the NotSearched is raised when the search fails.
I would like to combine the FoundService and NotSerched events, into an observable of NSNetService.
My current implementation looks like this:
public IObservable<NSNetService> Search()
{
var foundObservable = Observable
.FromEventPattern<NSNetServiceEventArgs>(
h => serviceBrowser.FoundService += h,
h => serviceBrowser.FoundService -= h)
.Select(x => x.EventArgs);
var notSearchedObservable = Observable
.FromEventPattern<NSNetServiceErrorEventArgs>(
h => serviceBrowser.NotSearched += h,
h => serviceBrowser.NotSearched -= h)
.Select(x => x.EventArgs);
var serviceObservable = Observable.Create(
(IObserver<NSNetServiceEventArgs> observer) =>
{
notSearchedObservable.Subscribe(n =>
{
string errorMessage = $"Search for {serviceType} failed:";
foreach (var kv in n.Errors)
{
log.Error($"\t{kv.Key}: {kv.Value}");
errorMessage += $" ({kv.Key}, {kv.Value})";
}
observer.OnError(new Exception(errorMessage));
});
foundObservable.Subscribe(observer);
return System.Reactive.Disposables.Disposable.Empty;
}).Select(x => x.Service);
serviceBrowser.SearchForServices(serviceType, domain);
return serviceObservable;
}
The code looks clunky and I have a gut feeling I'm not using System.Reactive correctly? Is there a more elegant way to combine event pairs, where one is producing and the other is signaling error? This is a common pattern in existing event based APIs in .NET.
Here is a small console app (depending only on System.Reactive) illustrating the type of API I want to Reactify:
using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace ReactiveLearning
{
class Program
{
static void Main(string[] args)
{
var browser = new ServiceBrowser();
var observableFound =
Observable.FromEventPattern<ServiceFoundEventArgs>(
h => browser.ServiceFound += h,
h => browser.ServiceFound -= h)
.Select(e => e.EventArgs.Service);
var observableError =
Observable.FromEventPattern<ServiceSearchErrorEventArgs>(
h => browser.ServiceError += h,
h => browser.ServiceError -= h);
var foundSub = observableFound.Subscribe(s =>
{
Console.WriteLine($"Found service: {s.Name}");
}, () =>
{
Console.WriteLine("Found Completed");
});
var errorSub = observableError.Subscribe(e =>
{
Console.WriteLine("ERROR!");
}, () =>
{
Console.WriteLine("Error Completed");
});
browser.Search();
Console.ReadLine();
foundSub.Dispose();
errorSub.Dispose();
Console.WriteLine();
}
}
class ServiceBrowser
{
public EventHandler<ServiceFoundEventArgs> ServiceFound;
public EventHandler<ServiceSearchErrorEventArgs> ServiceError;
public void Search()
{
Task.Run(async () =>
{
for (var i = 0; i < 5; ++i)
{
await Task.Delay(1000);
ServiceFound?.Invoke(this, new ServiceFoundEventArgs(new Service($"Service {i}")));
}
var r = new Random();
if (r.NextDouble() > 0.5)
{
ServiceError?.Invoke(this, new ServiceSearchErrorEventArgs());
}
});
}
}
class ServiceFoundEventArgs : EventArgs
{
public Service Service { get; private set; }
public ServiceFoundEventArgs(Service service) => Service = service;
}
class ServiceSearchErrorEventArgs : EventArgs {}
class Service
{
public event EventHandler<EventArgs> AddressResolved;
public event EventHandler<EventArgs> ErrorResolvingAddress;
public string Name { get; private set; }
public string Address { get; private set; }
public Service(string name) => Name = name;
public void ResolveAddress()
{
Task.Run(async () =>
{
await Task.Delay(500);
var r = new Random();
if (r.NextDouble() > 0.5)
{
Address = $"http://{Name}.com";
AddressResolved?.Invoke(this, EventArgs.Empty);
}
else
{
ErrorResolvingAddress?.Invoke(this, EventArgs.Empty);
}
});
}
}
}
Thank you for the excellent sample code. You need to make use of the excellent Materialize & Dematerialize operators. Here's how:
var observableFoundWithError =
observableFound
.Materialize()
.Merge(
observableError
.Materialize()
.Select(x =>
Notification
.CreateOnError<Service>(new Exception("Error"))))
.Dematerialize()
.Synchronize();
using (observableFoundWithError.Subscribe(
s => Console.WriteLine($"Found service: {s.Name}"),
ex => Console.WriteLine($"Found error: {ex.Message}"),
() => Console.WriteLine("Found Completed")))
{
browser.Search();
Console.ReadLine();
}
The Materialize() operator turns an IObservable<T> into and IObservable<Notification<T>> which allows the standard OnError and OnCompleted to be emitted through the OnNext call. You can use Notification.CreateOnError<T>(new Exception("Error")) to construct elements of observable which you can turn back into an IObservable<T> with Dematerialize().
I've thrown the Synchronize() to ensure that you've created a valid observable. The use of Materialize() does let you construct observables that don't follow the regular observable contract. Part of what Synchronize() does is just ensure only one OnError and only one OnCompleted and drops any OnNext that comes after either of the two.
Try this as a way to do what you wanted in the comments:
static void Main(string[] args)
{
var browser = new ServiceBrowser();
var observableFound =
Observable.FromEventPattern<ServiceFoundEventArgs>(
h => browser.ServiceFound += h,
h => browser.ServiceFound -= h)
.Select(e => e.EventArgs.Service);
var observableError =
Observable.FromEventPattern<ServiceSearchErrorEventArgs>(
h => browser.ServiceError += h,
h => browser.ServiceError -= h);
var observableFoundWithError = observableFound
.Materialize()
.Merge(
observableError
.Materialize()
.Select(x => Notification.CreateOnError<Service>(new Exception("Error"))))
.Dematerialize()
.Synchronize();
Func<Service, IObservable<Service>> resolveService = s =>
Observable.Create<Service>(o =>
{
var observableResolved = Observable.FromEventPattern<EventArgs>(
h => s.AddressResolved += h,
h => s.AddressResolved -= h);
var observableResolveError = Observable.FromEventPattern<EventArgs>(
h => s.ErrorResolvingAddress += h,
h => s.ErrorResolvingAddress -= h);
var observableResolvedWithError =
observableResolved
.Select(x => s)
.Materialize()
.Merge(
observableResolveError
.Do(e => Console.WriteLine($"Error resolving: {s.Name}"))
.Materialize()
.Select(x => Notification.CreateOnError<Service>(new Exception($"Error resolving address for service: {s.Name}"))))
.Dematerialize()
.Synchronize();
s.ResolveAddress();
return observableResolvedWithError.Subscribe(o);
});
using (
observableFoundWithError
.Select(s => resolveService(s))
.Switch()
.Subscribe(
s => Console.WriteLine($"Found and resolved service: {s.Name} ({s.Address})"),
ex => Console.WriteLine($"Found error: {ex.Message}"),
() => Console.WriteLine("Found Completed")))
{
browser.Search();
Console.ReadLine();
}
}
public class ServiceBrowser
{
public event EventHandler<ServiceFoundEventArgs> ServiceFound;
public event EventHandler<ServiceSearchErrorEventArgs> ServiceError;
public void Search() { }
}
public class Service
{
public event EventHandler<EventArgs> AddressResolved;
public event EventHandler<EventArgs> ErrorResolvingAddress;
public string Name;
public string Address;
public void ResolveAddress() { }
}
public class ServiceFoundEventArgs : EventArgs
{
public Service Service;
}
public class ServiceSearchErrorEventArgs : EventArgs
{
}
I might require a bit of tweaking - perhaps an Observable.Delay in there. Let me know if it works.
Related
I'm trying to register consumers but no success using mass transit.
I registered MT using Autofac using module approach.
Firstly - I created some simple message:
public class SimpleMessage
{
public string msg { get; set; }
}
and I've managed to send them into queue:
var endpointTest = await _busControl.GetSendEndpoint(new Uri("queue:queueTest"));
await endpointTest.Send(new SimpleMessage
{
msg = "test"
});
Then I created a consumer:
public class SimpleMessageConsumer : IConsumer<SimpleMessage>
{
private readonly ILogger _logger;
public SimpleMessageConsumer(ILogger logger)
{
_logger = logger;
}
public async Task Consume(ConsumeContext<SimpleMessage> context)
{
_logger.Info($"got msg from queue: {context.Message}");
}
}
But it won't run when the message appeared in the queue. My configuration is:
public class BusModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<BusSettings>().As<IBusSettings>();
builder.AddMassTransit(cfg =>
{
cfg.AddConsumer<SimpleMessageConsumer, SimpleMessageConsumerDefinition>();
cfg.Builder.Register(context =>
{
var busSettings = context.Resolve<IBusSettings>();
var logger = context.Resolve < ILogger >();
var busControl = Bus.Factory.CreateUsingRabbitMq(bus =>
{
bus.AutoDelete = busSettings.AutoDelete;
bus.Durable = busSettings.Durable;
bus.Exclusive = busSettings.Exclusive;
bus.ExchangeType = busSettings.Type;
//bus.UseNServiceBusJsonSerializer();
bus.Host(busSettings.HostAddress, busSettings.Port, busSettings.VirtualHost, null, h =>
{
h.Username(busSettings.Username);
h.Password(busSettings.Password);
});
bus.ReceiveEndpoint("queueTest", ec =>
{
ec.Consumer(() => new SimpleMessageConsumer(logger));
});
});
return busControl;
}).SingleInstance().As<IBusControl>().As<IBus>();
});
}
}
in program.cs
I have:
services.AddMassTransitHostedService();
and
containerBuilder.RegisterModule<BusModule>();
Such I mentioned - sending a msg to queue works but consumer wasn't running.
Can you help me what did I do wrong? how should I fix the configuration? in order to activate the consumer?
I've updated your configuration to work properly, using the actual bus configuration methods instead of mixing the two solutions:
public class BusModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<BusSettings>().As<IBusSettings>();
builder.AddMassTransit(cfg =>
{
cfg.AddConsumer<SimpleMessageConsumer, SimpleMessageConsumerDefinition>();
cfg.UsingRabbitMq((context,cfg) =>
{
var busSettings = context.GetRequiredService<IBusSettings>();
var logger = context.GetRequiredService<ILogger>();
//bus.UseNServiceBusJsonSerializer();
bus.Host(busSettings.HostAddress, busSettings.Port, busSettings.VirtualHost, null, h =>
{
h.Username(busSettings.Username);
h.Password(busSettings.Password);
});
bus.ReceiveEndpoint("queueTest", ec =>
{
// i'm guessing these apply to the receive endpoint, not the bus endpoint
ec.AutoDelete = busSettings.AutoDelete;
ec.Durable = busSettings.Durable;
ec.Exclusive = busSettings.Exclusive;
ec.ExchangeType = busSettings.Type;
ec.ConfigureConsumer<SimpleMessageConsumer>(context);
});
});
});
}
}
I want to read data from database. For this I create a query and queryhandler classes
QueryHandler
public class OrderGetQueryHandler: IQueryHandler<OrderGetQuery, OrderDTO>
{
private readonly GoodWillWebDbContext _context;
private readonly IQueryDispatcher _queryDispatcher;
public OrderGetQueryHandler(GoodWillWebDbContext context, IQueryDispatcher queryDispatcher)
{
_context = context;
_queryDispatcher = queryDispatcher;
}
private bool CheckPartnerBlock(BlockTypes blockType, decimal debtOverdue, bool payOff)
{
if (blockType == BlockTypes.Block)
return true;
if (blockType == BlockTypes.NotBlock)
return false;
if (blockType == BlockTypes.PreliminaryPayment)
return payOff;
return debtOverdue <= 0;
}
public async Task<OrderDTO> HandleAsync(OrderGetQuery query)
{
var order = await _context.Orders.FindAsync(query.OrderID);
if (order != null)
{
var getCustomerTask = _context.Partners.FindAsync(order.CustomerID).AsTask();
var getCuratorTask = _context.Users.FindAsync(order.CuratorID).AsTask();
var getPaymentTask = _context.Payments.OrderByDescending(x => x.PaymentID).FirstOrDefaultAsync(x => x.CustomerID == order.CustomerID);
var getOrderLinesTask =
_queryDispatcher.HandleAsync<OrderLinesGetQuery, OrderLineDTO[]>(
new OrderLinesGetQuery(query.OrderID));
await Task.WhenAll(getCustomerTask, getCuratorTask, getOrderLinesTask, getPaymentTask);
var priceRange = await _context.PriceRanges.FindAsync(getCustomerTask.Result.PriceRangeID);
return new OrderDTO
(
order.OrderID,
getCustomerTask.Result.Name,
getOrderLinesTask.Result,
order.CustomerID,
order.OrderStateID,
order.CanDelete,
order.CreationDate,
getPaymentTask.Result.DebtBank,
getPaymentTask.Result.DebtOverdue,
this.CheckPartnerBlock(getCustomerTask.Result.BlockTypeID, getPaymentTask.Result.DebtOverdue, order.PayOff),
priceRange.Name,
order.ReservationDate,
Mapper.Convert<DeliveryInfoDTO, BaseEntities.Entities.Sales.Order>(order)
);
}
throw new NullReferenceException();
}
}
this queryhandler i use in ASP.NET WEB Application. My startup class is
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
string connection = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<GoodWillWebDbContext>(options =>
options.UseSqlServer(connection), ServiceLifetime.Transient);
services.AddScoped<IQueryHandler<OrdersGetQuery, BaseEntities.DTO.Sales.Order.OrderDTO[]>, OrdersGetQueryHandler>();
services.AddScoped<IQueryHandler<OrderGetQuery, Sales.Queries.DTO.Order.OrderDTO>, OrderGetQueryHandler>();
services.AddScoped<ICommandDispatcher, CommandDispatcher>();
services.AddScoped<IQueryDispatcher, QueryDispatcher>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
I set ServiceLifetime.Transient for my context, but I still get an exception: InvalidOperationException A second operation started on this context before a previous operation completed.
What's wrong?
It seems you're running multiple operations on the context without waiting for the previous ones to end, which EF doesn't like:
var getCustomerTask = _context.Partners.FindAsync(order.CustomerID).AsTask();
var getCuratorTask = _context.Users.FindAsync(order.CuratorID).AsTask();
var getPaymentTask = _context.Payments.OrderByDescending(x => x.PaymentID).FirstOrDefaultAsync(x => x.CustomerID == order.CustomerID);
Either make these call sync or use the await keyword.
What I want is create caching proxies for all my components decorated with some attributes. So, I made Autofac Module like this:
public class CachingModule : Autofac.Module
{
private readonly ProxyGenerator generator;
public CachingModule()
{
generator = new ProxyGenerator();
}
protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
{
var type = registration.Activator.LimitType;
if (type.GetCustomAttribute<CachedAttribute>(true) != null
|| type.GetMethods().Any(m => m.GetCustomAttribute<CachedAttribute>(true) != null))
{
registration.Activating += (s, e) =>
{
var proxy = generator.CreateClassProxyWithTarget(e.Instance.GetType(),
e.Instance,
interceptors: e.Context.Resolve<IEnumerable<CacheInterceptor>>().ToArray());
e.ReplaceInstance(proxy);
};
}
}
}
What I can't get to work is: I can't create proxy instances with parametrized constructors, is there any way to do this?
Ok, I've managed to get it all working, so for anyone interested here is a sample of proxy generation with non-default constructors
public class CachingModule : Autofac.Module
{
private readonly ProxyGenerator generator;
public CachingModule()
{
generator = new ProxyGenerator();
}
protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
{
var type = registration.Activator.LimitType;
if (type.GetCustomAttribute<CachedAttribute>(true) != null
|| type.GetMethods().Any(m => m.GetCustomAttribute<CachedAttribute>(true) != null))
{
var ctors = type.GetConstructors();
registration.Activating += (s, e) =>
{
if (e.Instance is IProxyTargetAccessor)
{
return;
}
var param = new List<object>();
var infos = ctors.First().GetParameters();
if (ctors.Length > 0 && infos.Length > 0)
{
foreach (var p in e.Parameters)
{
foreach (var info in infos)
{
if (p.CanSupplyValue(info, e.Context, out var valueProvider))
{
param.Add(valueProvider());
}
}
}
}
var instance = e.Instance;
var toProxy = instance.GetType();
var proxyGenerationOptions = new ProxyGenerationOptions(new CacheProxyGenerationHook());
var proxy = generator.CreateClassProxyWithTarget(toProxy,
instance,
proxyGenerationOptions,
param.ToArray(),
interceptors: e.Context.Resolve<IEnumerable<CacheInterceptor>>().ToArray());
e.ReplaceInstance(proxy);
};
}
}
}
This implementation is bad since it will end up disposing the SubscriberSocket when the first subscription terminates. When I run it, nothing seems to publish. [This uses Rx 3.0.0]
How to modify Receive function to fix this problem?
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using NetMQ;
using NetMQ.Sockets;
using System.Reactive.Linq;
namespace App1
{
class MainClass
{
// publisher for testing, should be an external data publisher in real environment
public static Thread StartPublisher(PublisherSocket s)
{
s.Bind("inproc://test");
var thr = new Thread(() =>
{
Console.WriteLine("Start publishing...");
while (true)
{
Thread.Sleep(500);
bool more = false;
s.SendFrame("hello", more);
}
});
thr.Start();
return thr;
}
public static IObservable<string> Receive(SubscriberSocket subp)
{
return Observable
.Create<string>(o =>
Observable.Using<string, SubscriberSocket>(() =>
{
subp.Connect("inproc://test");
subp.Subscribe("");
return subp;
}, sub =>
Observable
.FromEventPattern<EventHandler<NetMQSocketEventArgs>, NetMQSocketEventArgs>(
h => sub.ReceiveReady += h,
h => sub.ReceiveReady -= h)
.Select(x => sub.ReceiveFrameString()))
.Subscribe(o));
}
public static void Main(string[] args)
{
var sub = new SubscriberSocket();
var pub = new PublisherSocket();
StartPublisher(pub);
Receive(sub).Subscribe(Console.WriteLine);
Console.ReadLine();
}
}
}
It seems to me that you should be doing something like this:
void Main()
{
var address = "inproc://test";
var pub = new PublisherSocket();
pub.Bind(address);
var pubSubscription =
Observable
.Interval(TimeSpan.FromMilliseconds(500.0))
.Subscribe(n => pub.SendFrame("hello", false));
Receive(address).Subscribe(Console.WriteLine);
Console.ReadLine();
}
public static IObservable<string> Receive(string address)
{
return Observable
.Create<string>(o =>
Observable.Using<string, SubscriberSocket>(() =>
{
var ss = new SubscriberSocket();
ss.Connect(address);
ss.Subscribe("");
return ss;
}, ss =>
Observable
.FromEventPattern<EventHandler<NetMQSocketEventArgs>, NetMQSocketEventArgs>(
h => ss.ReceiveReady += h,
h => ss.ReceiveReady -= h)
.Select(x => ss.ReceiveFrameString()))
.Subscribe(o));
}
Primarily, this means creating the SubscriberSocket inside the Observable.Create - which is where it belongs.
I have a Hot stream of events coming of following type:
Event
{
string name;
int state ; // its 1 or 2 ie active or unactive
}
there is a function which provides parent name of given name - string GetParent(string name)
I need to buffer event per parent for 2 minutes, if during this 2 minute , i recv any event for child with state =2 for a given parent , this buffer should cancel and should output 0 otherwise i get the count of the events recvd .
I know I have to use GroupBy to partition, and then buffer and then count but i am unable to think of a way by which i create Buffer which is unique per parent, i though of using Distinct but this doesnt solve the problem, for i only dont want to create buffer till the parent is active (as once the parent's buffer gets cancelled or 2 minutes is over, the parent buffer can be created again)
So I understand I need to create a custom buffer which checks the condition for creating buffer, but how do i do this via reactive extensions.
Any help will be greatly appreciated.
regards
Thanks Brandon for your help. This is the main program I am using for testing. Its not working.As I am new to reactive extension problem can be in the way i am testing
namespace TestReactive
{
class Program
{
static int abc = 1;
static void Main(string[] args)
{
Subject<AEvent> memberAdded = new Subject<AEvent>();
//ISubject<AEvent, AEvent> syncedSubject = new ISubject<AEvent, AEvent>();
var timer = new Timer { Interval = 5 };
timer.Enabled = true;
timer.Elapsed += (sender, e) => MyElapsedMethod(sender, e, memberAdded);
var bc = memberAdded.Subscribe();
var cdc = memberAdded.GroupBy(e => e.parent)
.SelectMany(parentGroup =>
{
var children = parentGroup.Publish().RefCount();
var inactiveChild = children.SkipWhile(c => c.state != 2).Take(1).Select(c => 0);
var timer1 = Observable.Timer(TimeSpan.FromSeconds(1));
var activeCount = children.TakeUntil(timer1).Count();
return Observable.Amb(activeCount, inactiveChild)
.Select(count => new { ParentName = parentGroup.Key, Count = count });
});
Observable.ForEachAsync(cdc, x => WriteMe("Dum Dum " + x.ParentName+x.Count));
// group.Dump("Dum");
Console.ReadKey();
}
static void WriteMe(string sb)
{
Console.WriteLine(sb);
}
static void MyElapsedMethod(object sender, ElapsedEventArgs e, Subject<AEvent> s)
{
AEvent ab = HelperMethods.GetAlarm();
Console.WriteLine(abc + " p =" + ab.parent + ", c = " + ab.name + " ,s = " + ab.state);
s.OnNext(ab);
}
}
}
public static AEvent GetAlarm()
{
if (gp> 4)
gp = 1;
if (p > 4)
p = 1;
if (c > 4)
c = 1;
AEvent a = new AEvent();
a.parent = "P" + gp + p;
a.name = "C" + gp + p + c;
if (containedKeys.ContainsKey(a.name))
{
a.state = containedKeys[a.name];
if (a.state == 1)
containedKeys[a.name] = 2;
else
containedKeys[a.name] = 1;
}
else
{
containedKeys.TryAdd(a.name, 1);
}
gp++; p++; c++;
return a;
}
So this method , generates a event for Parent at each tick. It generates event for parent P11,P22,P33,P44 with State =1 and then followed by events for Parent P11,P22,P33,P44 with State =2
I am using Observable.ForEach to print the result, I see its being called 4 times and after that its nothing, its like cancellation of group is not happening
Assuming that a two minute buffer for each group should open as soon as the first event for that group is seen, and close after two minutes or a zero state is seen, then I think the following works:
public static IObservable<EventCount> EventCountByParent(
this IObservable<Event> source, IScheduler scheduler)
{
return Observable.Create<EventCount>(observer => source.GroupByUntil(
evt => GetParent(evt.Name),
evt => evt,
group =>
#group.Where(evt => evt.State == 2)
.Merge(Observable.Timer(
TimeSpan.FromMinutes(2), scheduler).Select(_ => Event.Null)))
.SelectMany(
go =>
go.Aggregate(0, (acc, evt) => (evt.State == 2 ? 0 : acc + 1))
.Select(count => new EventCount(go.Key, count))).Subscribe(observer));
}
With EventCount (implementing equality overrides for testing) as:
public class EventCount
{
private readonly string _name;
private readonly int _count;
public EventCount(string name, int count)
{
_name = name;
_count = count;
}
public string Name { get { return _name; } }
public int Count { get { return _count; } }
public override string ToString()
{
return string.Format("Name: {0}, Count: {1}", _name, _count);
}
protected bool Equals(EventCount other)
{
return string.Equals(_name, other._name) && _count == other._count;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((EventCount) obj);
}
public override int GetHashCode()
{
unchecked
{
return ((_name != null ? _name.GetHashCode() : 0)*397) ^ _count;
}
}
}
And Event as:
public class Event
{
public static Event Null = new Event(string.Empty, 0);
private readonly string _name;
private readonly int _state;
public Event(string name, int state)
{
_name = name;
_state = state;
}
public string Name { get { return _name; } }
public int State { get { return _state; } }
}
I did a quick (i.e. not exhaustive) test with Rx-Testing:
public class EventCountByParentTests : ReactiveTest
{
private readonly TestScheduler _testScheduler;
public EventCountByParentTests()
{
_testScheduler = new TestScheduler();
}
[Fact]
public void IsCorrect()
{
var source = _testScheduler.CreateHotObservable(
OnNext(TimeSpan.FromSeconds(10).Ticks, new Event("A", 1)),
OnNext(TimeSpan.FromSeconds(20).Ticks, new Event("B", 1)),
OnNext(TimeSpan.FromSeconds(30).Ticks, new Event("A", 1)),
OnNext(TimeSpan.FromSeconds(40).Ticks, new Event("B", 1)),
OnNext(TimeSpan.FromSeconds(50).Ticks, new Event("A", 1)),
OnNext(TimeSpan.FromSeconds(60).Ticks, new Event("B", 2)),
OnNext(TimeSpan.FromSeconds(70).Ticks, new Event("A", 1)),
OnNext(TimeSpan.FromSeconds(140).Ticks, new Event("A", 1)),
OnNext(TimeSpan.FromSeconds(150).Ticks, new Event("A", 1)));
var results = _testScheduler.CreateObserver<EventCount>();
var sut = source.EventCountByParent(_testScheduler).Subscribe(results);
_testScheduler.Start();
results.Messages.AssertEqual(
OnNext(TimeSpan.FromSeconds(60).Ticks, new EventCount("B", 0)),
OnNext(TimeSpan.FromSeconds(130).Ticks, new EventCount("A", 4)),
OnNext(TimeSpan.FromSeconds(260).Ticks, new EventCount("A", 2)));
}
}
something like....
source.GroupBy(e => GetParent(e.name))
.SelectMany(parentGroup =>
{
var children = parentGroup.Publish().RefCount();
var inactiveChild = children.SkipWhile(c => c.state != 2).Take(1).Select(c => 0);
var timer = Observable.Timer(TimeSpan.FromMinutes(2));
var activeCount = children.TakeUntil(timer).Count();
return Observable.Amb(activeCount, inactiveChild)
.Select(count => new { ParentName = parentGroup.Key, Count = count };
});
This will give you a sequence of { ParentName, Count } objects.