5 ms·
I love channels in .NET. I use them in our .NET web services to pass work items from controllers to background services without having to directly couple the co
by FroshKiller 3y ago
I love channels in .NET. I use them in our .NET web services to pass work items from controllers to background services without having to directly couple the controller & background service classes.
- peheje 3y agoHow do you do this? Does the controller then not know what the receiver is or wants in any way? Surely the controller needs to put something on the channel in the correct format?
- pjc50 3y agoThe channels are typed: https://learn.microsoft.com/en-us/dotnet/api/system.threading.channels.channel-2.writer?view=net-7.0 https://learn.microsoft.com/en-us/dotnet/api/system.threadin...
- FroshKiller 3y agoYes, the controller consumes an instance of ChannelWriter<T> where T is a job type, and the service consumes an instance of ChannelReader<T>. With basic dependency injection, this means you have to set up & register different channels with different types if you need to coordinate with multiple services, but I think that's a small price to pay. Having a job class with a type parameter for its encapsulated workload saves you from having to actually write different job classes, at least. It's trivial to write a generic extension method that takes the encapsulated type as a parameter, sets up the channel with your preferred options, and registers the reader & writer for DI. Then you have a one-liner for adding this kind of channel support to your projects.
- peheje 3y agoWhat are some of the benefits of this approach compared to calling an interface of your services returning Task<T> if needed from you controller? Some of the benefits of this in my view is: - Clear view of how many dependencies a controller has, though the dependencies are abstractions - Easy to Control+Click into the service in your IDE. - Easy to "Find all usages" of the service. - Easy to mock in unit tests, just mock the interface - Can be decorated if needed with Scrutor - Fast, just a method call - Leverages Task if async is required, easy to e.g. call a service get id returned to use to call another service. How would you do these things with ChannelWriter and ChannelReader, do you have an example of this approach somewhere?
- FroshKiller 3y agoI'm glad you asked, because I should clarify that my use case is "fire and forget" tasks. The controller is not interested in the result of the service's operation, so there is no need for any return value. Regarding your other points: Dependencies can be traced by the job type associated with the channel. It is somewhat indirect but no more indirect than an interface, really. Since I am not concerned with results, mocking is even easier. I just have a service implementation that consumes from a ChannelReader and discards the result. This is really a single class since it can take a type parameter if that makes sense--no need for separate mocks. I haven't used Scrutor, so I can't speak to that. Reading and writing from a channel are also just method calls. There are synchronous and asynchronous options for both. I don't have any open code to share, but I found the design very intuitive. If you read the documentation and follow your intuition, you will probably implement things exactly as I did. Also I am not trying to convince anyone that channels are the best answer! I just found them helpful in my projects and think they have not been promoted very well in the .NET sphere.
- hypeatei 3y agoThe widely used MediatR library[0] could be used to do that as well, just FYI. [0]: https://github.com/jbogard/MediatR https://github.com/jbogard/MediatR
- neonsunset 3y agoFor the love of God, do not use MediatR. Especially in scenarios Channels are designed for. It is mentally confusing and bloated abstraction, in most places simply resulting in more code achieving the same result at 0.25x(best case) speed.
- gokhan 3y agoIt's not for the speed but separating parts of the same monolithic program into a microservice like structure.
- neonsunset 3y agoThis one, that is the problem. It makes it impossible to logically follow (without reviewing all type references) the flow of execution - just call a service directly instead of inventing 3 contexts and 2 intermediate message handlers that end up just passing the data to one consumer at the end. The fact that this does not raise eyebrows is one of the (avoidable) reasons, among many, why C# gets bad rep. Go solves this by the virtue of its design that makes it painful to invent big brain solutions that should have stayed simple. This is where C# has much longer learning curve and requires judgement when applying the tools at hand.
- dgellow 3y agoI never heard of MediatR before today, but what do you think of message buses like IMessenger from MVVM Toolkit? https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/messenger https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mv... I've been mostly using C# for GUI applications and recently have been thinking about relying on a message bus as the main communication device between services and view models. As you mentions that complicates the flow of logic and ones loses benefits of explicitly listing your dependencies you get by just injecting all your services via constructor parameters. But that makes it simpler to refactor complex view models and services without impacting the rest of the codebase (at least in theory, it's of course often messier in practice...).
- KRAKRISMOTT 3y agoWhy didn't Microsoft implement the arrow syntax for channels? It makes it much less confusing than passing it as parameters.
- dgellow 3y agoChannels in C# are just a regular library, it would be overkill to introduce syntax changes to the language just for this. Btw, you have other ways to get the producer-consumer pattern in .Net, Channels are just one of the various options but you also have BufferBlock from System.Threading.Tasks.Dataflow (with a more complicated API). https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/how-to-write-messages-to-and-read-messages-from-a-dataflow-block https://learn.microsoft.com/en-us/dotnet/standard/parallel-p...
- FroshKiller 3y agoPrior to channels, we'd add a ConcurrentQueue<T> from System.Collections.Concurrent to the service then expose a public method that enqueued an instance of T. Controllers then received the singleton instance of the services through DI and would invoke that method to add a job item to the service's queue. Obviously, that created a direct coupling. You could set up an interface and have the controller consume the interface instead, but it was still a coupling in practice. I suppose you register the public method as a delegate or something instead and just inject the delegate, but that's a bit hacky to me, and I think my juniors would find it confusing.
- WorldMaker 3y agoIf you really wanted to you could create a simple Channel wrapper that operator overloads some visually arrow-like operator, exactly such as C++'s famous stream overloads using << and >> byte shift operators as "stream operators". That sort of "cutesy" operator overloading is generally frowned upon in C# best practices, but still possible if you don't mind some C# developers either frowning or laughing at you.