I do search in several places at once.

Back at the level, while something like:

foreach (var item in Plugins.SelectMany(p => p.Search(query))) Items.Add(new ViewModel(item)); 

And in each implementation of the Search method:

 public override IEnumerable<Item> Search(string name) { await some resource foreach (var element in networkResult) ... yield return result; } 

In fact, I want parallel access to all searches, so that each element appears in the UI when it is ready, and not when everything is completely finished, as it is now working. What exactly is wrapped up in the task? There are no good ideas. From the outside, Task<IEnumerable> looks more logical, but I don’t understand how to implement it correctly.

  • one
    Why is there a transfer at all? Add results to the collection you need right inside the asynchronous function - tym32167
  • @ tym32167 from the application point of view - the search returns results, and what the client does to them is another matter. - Monk
  • one
    At the moment - only manually. In the next version of the framework, IAsyncEnumerable will appear, it will be easier with it. - VladD
  • @VladD and see where it is possible, how to do it manually? I still get Frankenstein = _ = - Monk
  • one
    Well, in order not to do everything manually, there is IX ( github.com/Reactive-Extensions/Rx.NET/tree/develop/Ix.NET/… ). Well, or another found ( github.com/tyrotoxin/AsyncEnumerable ), not sure about that. - VladD

1 answer 1

See, it's not that hard.

You install the Ix, System.Interactive and System.Interactive.Async nuget packages. You get the IAsyncEnumerable interface and helper classes.

You can use it like this:

 static class FileEx { public static IAsyncEnumerable<string> ReadLinesAsync(string path) { // IAsyncEnumerable<T> обладает только одной функцией - создать энумератор return AsyncEnumerable.CreateEnumerable(() => { var stream = File.OpenText(path); string current = null; // создаём энумератор при помощи готовой фабрики return AsyncEnumerable.CreateEnumerator( // у StreamReader.ReadLineAsync нет перегрузки с ct, пичалько moveNext: async ct => (current = await stream.ReadLineAsync()) != null, current: () => current, dispose: stream.Dispose); }); } } 

See what happens here. To begin with, the same sequence can be run over by different pieces of code alternately, so we keep the state of the current round in the enumerator. In principle, we would need to have a separate class for the enumerator, and keep properties in it. But we will go more fashionable way, and we will keep the data in the circuit. We open StreamReader , we get a variable for the current line.

The asynchronous function MoveNext iterator gets the next line from StreamReader 's, and checks the result for null ( null means the end of the file). The Current function simply returns the current line. And Dispose closes the stream at the end.

Now you can work with this:

 class Program { static async Task Main(string[] args) { var lines = FileEx.ReadLinesAsync("text.txt"); using (var en = lines.GetEnumerator()) { while (await en.MoveNext()) Console.WriteLine(en.Current); } } } 

Or simply

 await FileEx.ReadLinesAsync("text.txt") .ForEachAsync(s => Console.WriteLine(s)); 

In the next version of C #, support for asynchronous enumerators is planned right in the language. With her, our example is written as:

 static class FileEx { public static async IAsyncEnumerable<string> ReadLinesAsync(string path) { using (var stream = File.OpenText(path)) { string current; while ((current = await stream.ReadLineAsync()) != null) yield return current; }; } } class Program { static async Task Main(string[] args) { foreach await (var s in FileEx.ReadLinesAsync("text.txt")) Console.WriteLine(s); } } 

See, for your particular case ( this is the code ) you should parse the task into its component parts, since there are a lot of asynchronous pieces in it. Then you can link them instead.

Let's start by getting the pages and parsing them. It’s easy to get a list of hosts, asynchrony is not needed here:

 var hosts = ConfigStorage.Plugins .Where(p => p.GetParser().GetType() == typeof(Parser)) .Select(p => p.GetSettings().MainUri); 

Now, we need to get a list of HtmlNodeCollection by host. This is a “long” task; we take it to task:

 async Task<HtmlNodeCollection> GetHostMangasAsync(string name, Uri host, CookieClient client) { var searchHost = new Uri(host, "search?q=" + WebUtility.UrlEncode(name)); var page = await Task.Run(() => Page.GetPage(searchHost, client)); if (!page.HasContent) return null; return await Task.Run(() => { var document = new HtmlDocument(); document.LoadHtml(page.Content); return document.DocumentNode.SelectNodes("//div[@class='tile col-sm-6']"); }); } 

Now, we need from the non-asynchronous sequence of hosts and Task , which receives from each host HtmlNodeCollection , to get an asynchronous sequence. I did not find such a method in Ix out of the box, but it is easy to put together it myself. Let us make it generalized, suddenly it will be needed. The code is almost the same as the File.ReadLinesAsync example.

 static class AsyncEnumerableExtensions { public static IAsyncEnumerable<R> SelectAsync<T, R>( this IEnumerable<T> seq, Func<T, Task<R>> selector) { return AsyncEnumerable.CreateEnumerable(() => { IEnumerator<T> seqEnum = seq.GetEnumerator(); R current = default; return AsyncEnumerable.CreateEnumerator( moveNext: async ct => { if (!seqEnum.MoveNext()) return false; current = await selector(seqEnum.Current); return true; }, current: () => current, dispose: seqEnum.Dispose); }); } } 

Armed with this, we can write this:

 IAsyncEnumerable<HtmlNodeCollection> GetSearchPages(string name) { var hosts = ConfigStorage.Plugins .Where(p => p.GetParser().GetType() == typeof(Parser)) .Select(p => p.GetSettings().MainUri); var client = new CookieClient(); return hosts.SelectAsync(host => GetHostMangasAsync(name, host, client))) .Where(nc => nc != null); } 

Checking for null needed because GetHostMangasAsync can return null .

Great, move on. So, we again have a non-asynchronous HtmlNodeCollection collection, from each element of which we can pull an instance of IManga using an asynchronous function (because we have access to the network). Write the code:

 async Task<IManga> GetMangaFromNode(Uri host, CookieClient client, HtmlNode manga) { // Это переводчик, идем дальше. if (manga.SelectSingleNode(".//i[@class='fa fa-user text-info']") != null) return null; var image = manga.SelectSingleNode(".//div[@class='img']//a//img"); var imageUri = image?.Attributes.Single(a => a.Name == "data-original").Value; var mangaNode = manga.SelectSingleNode(".//h3//a"); var mangaUri = mangaNode.Attributes.Single(a => a.Name == "href").Value; var mangaName = mangaNode.Attributes.Single(a => a.Name == "title").Value; if (!Uri.TryCreate(mangaUri, UriKind.Relative, out Uri test)) return null; var result = Mangas.Create(new Uri(host, mangaUri)); result.Name = WebUtility.HtmlDecode(mangaName); if (imageUri != null) result.Cover = await client.DownloadDataAsync(imageUri); return result; } 

We need to connect them now. It is not difficult. The only problem is that GetMangaFromNode also needs a CookieClient , and it is hidden from us inside GetSearchPages . OK, we will pass it outside. Then, we only HtmlNodeCollection , and we also need a host . Modify GetSearchPages : we will return pairs from the host and the HtmlNode collection, and take as input the CookieClient :

 IAsyncEnumerable<(Uri host, HtmlNodeCollection nodes)> GetSearchPages( string name, CookieClient client) { var hosts = ConfigStorage.Plugins .Where(p => p.GetParser().GetType() == typeof(Parser)) .Select(p => p.GetSettings().MainUri); return hosts.SelectAsync( async host => (host, nodes: await GetHostMangasAsync(name, host, client))) .Where(pair => pair.nodes != null); } 

Well, combine. With each asynchronous function, each synchronous collection of HtmlNode gives us a collection of instances of IManga . This is done again with our SelectAsync :

 IAsyncEnumerable<IManga> GetFromHostAndNodes( Uri host, HtmlNodeCollection nodes, CookieClient client) => nodes.SelectAsync(node => GetMangaFromNode(host, client, node)); 

Now you can add a puzzle:

 public IAsyncEnumerable<IManga> Search(string name) { var client = new CookieClient(); return GetSearchPages(name, client) .SelectMany(pair => GetFromHostAndNodes(pair.host, pair.nodes, client)) .Where(m => m != null); } 

Everything!

  • And where you can read centrally about all the new innovations in the future edition of the language? - iluxa1810
  • @ iluxa1810: Well, there are no guarantees about new features. Information appears here and here , and also here and here . But there is no centralized information, you have to catch a little bit. - VladD
  • I read it and tried it myself. Unfortunately, the code you brought me doesn’t help much, but here I’m to blame myself, I didn’t describe the question well. The bottom line is that in synchronous code, there is a yield return that interrupts execution, by analogy with this interruption, I want the IAsyncEnumerable to work. And so - I had to make an analogue of File.Open , which will prepare all the data quite synchronously, and then with a separate method like ReadLineAsync add data with heavy queries if necessary. In fact, this could be done without any third-party libraries, or why don’t I understand? - Monk
  • @Monk: Well, yes, to work together with yield and await is not so easy, and it will be available just in the next version of the language (see the code below). In my example, File.Open is synchronous, but it is lightweight, and heavy chunks are ReadLineAsync , they access the file system. In fact, it could be done without libraries, well, everything can be done manually. Asynchronous action is still on you. - VladD
  • @Monk: You better show what your code is and what you would like to get. - VladD