1

I am developing a dictionary app which has many interconnected tables, so when a user makes a query the app searches through almost all of those tables and across many their fields, which results in heavy queries to the database and UI freezes.

So, how can I make these queries in a separate task asynchronously each time updating the UI with the results?

I've seen findAllAsync and the like for android version of the realm but for .net I couldn't find any alternatives, I tried to reinitialize as suggested elsewhere the database each time I run the async, but somehow it doesn't work and give me the same error.

System.Exception: 'Realm accessed from incorrect thread.'

The error gets thrown on ToList() when I try to convert the realm results to normal list to handle on UI, please help to fix this behavior

Here is my code

using Data.Models;
using Microsoft.EntityFrameworkCore.Internal;
using Realms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Data
{
   public class RealmAsync
{
    IQueryable<Entry> Entries { get; set; }

    public async void Run()
    {
        Realm RealmDatabase = Realm.GetInstance();
        Entries = RealmDatabase.All<Entry>();

        var entries = await FindAsync("а", Entries);
        Console.WriteLine($"async result async {entries.Count()}");
    }
    public async Task<IEnumerable<Entry>> FindAsync(string needle, IQueryable<Entry> haystack)
    {
        var foregroundRealm = Realm.GetInstance();
        var haystackRef = ThreadSafeReference.Create<Entry>(haystack);

        var filteredEntriesRef = await Task.Run(() =>
        {
            using (var realm = Realm.GetInstance())
            {
                var bgHaystack = realm.ResolveReference(haystackRef);
                return ThreadSafeReference.Create(bgHaystack.Where(entry => entry.Content.ToLower().StartsWith(needle)));
            }
        });

        var result = foregroundRealm.ResolveReference(filteredEntriesRef).ToArray();
        return result;
    }
}

Entry model class:

using System.ComponentModel.DataAnnotations.Schema;
using Realms;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System;

namespace Data.Models
{
    [Table("entry")]
    public class Entry : RealmObject
    {
        public class EntryType
        {
            public const byte Word = 1;
            public const byte Phrase = 2;
            public const byte Text = 3;
        };

        [Key]
        [PrimaryKey]
        [Column("entry_id")]
        public int Id { get; set; }

        [Column("user_id")]
        public int UserId { get; set; }

        [Column("source_id")]
        public int SourceId { get; set; }

        [Indexed]
        [Column("type")]
        public byte Type { get; set; }

        [Column("rate")]
        public int Rate { get; set; }

        [Column("created_at")]
        public string CreatedAt { get; set; }

        [Column("updated_at")]
        public string UpdatedAt { get; set; }

        [NotMapped]
        public Phrase Phrase { get; set; }

        [NotMapped]
        public Word Word { get; set; }

        [NotMapped]
        public Text Text { get; set; }

        [NotMapped]
        public IList<Translation> Translations { get; }

        [NotMapped]
        public string Content
        {
            get {
                switch (Type)
                {
                    case EntryType.Phrase:
                        return Phrase?.Content;
                    case EntryType.Word:
                        return Word?.Content;
                    case EntryType.Text:
                        return Text?.Content;
                }
                return "";
            }
        }
    }
}
Movsar Bekaev
  • 862
  • 1
  • 11
  • 26
  • My advice: choose between `Result` and `await`. Use one or the other, not both. Otherwise ... deadlocks. And if you decide to use the later, [avoid async void](https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void). – Theodor Zoulias May 06 '20 at 00:14
  • @TheodorZoulias I simplified the codebase, please check if you can help, I think it's more related to how Realm works though – Movsar Bekaev May 06 '20 at 00:17
  • No `Result` in the horizon now, so the primary suspect for a possible deadlock has been eliminated. The error that you get implies the existence of thread-affinity. You may find this question useful: [Forcing certain code to always run on the same thread](https://stackoverflow.com/questions/61530632/forcing-certain-code-to-always-run-on-the-same-thread/61536776). – Theodor Zoulias May 06 '20 at 00:24

3 Answers3

1

So, in case somebody needs this, here is how I ended up with my real implementation which works nicely (it's not the example code in the question, but I thought this can be more useful and practical) - let me know if you'll need more info about it:

 public async override Task<List<Entry>> FindAsync(string inputText)
        {
            // Checks for previous results to speed up the search while typing
            // For realm to be able to work cross-thread, when having previous results this has to be passed to the search task
            ThreadSafeReference.Query<Entry> previousResultsRef = null;
            if (PreviousInputText != null)
            {
                if ((PreviousInputText.Length == inputText.Length - 1) && (inputText == PreviousInputText + inputText.Last()))
                {
                    // Characters are being inserted
                    if (PreviousResults.ContainsKey(PreviousInputText.Length - 1))
                    {
                        previousResultsRef = ThreadSafeReference.Create(PreviousResults[PreviousInputText.Length - 1]);
                    }
                } else if ((PreviousInputText.Length == inputText.Length + 1) && (inputText == PreviousInputText.Substring(0, PreviousInputText.Length - 1)))
                {
                    // Characters are being removed
                    PreviousResults[PreviousInputText.Length] = null;
                    PreviousInputText = inputText;
                    return PreviousResults[PreviousInputText.Length - 1].ToList();
                }
            }

            // Receives reference to the search results from the dedicated task
            var resultingEntries = await Task.Run(() =>
            {
                // Preserves current input text for the further uses
                PreviousInputText = inputText;

                // Gets the source entries - either previous or all
                var sourceEntries = (previousResultsRef == null) ? Realm.GetInstance(DbPath).All<Entry>() : Realm.GetInstance(DbPath).ResolveReference(previousResultsRef);

                // Because realm doesn't support some of the LINQ operations on not stored fields (Content) 
                // the set of entries @sourceEntries is assigned to a IEnumerable through conversion when passing to Search method as a parameter
                IEnumerable<Entry> foundEntries = Search(inputText, sourceEntries);

                // Extracts ids
                var foundEntryIds = ExtractIdsFromEntries(foundEntries);

                if (foundEntryIds.Count() == 0)
                {
                    return null;
                }

                // Select entries
                var filteredEntries = FilterEntriesByIds(sourceEntries, foundEntryIds.ToArray());

                if (filteredEntries == null)
                {
                    // Something went wrong
                    return null;
                }

                // Creates reference for the main thread
                ThreadSafeReference.Query<Entry> filteredEntriesRef = ThreadSafeReference.Create(filteredEntries);
                return filteredEntriesRef;
            });

            if (resultingEntries == null)
            {
                // Next search will be a new search
                PreviousResults[inputText.Length - 1] = null;
                return new List<Entry>();
            }

            var results = RealmDatabase.ResolveReference(resultingEntries);

            // Preserve the current results and the input text for the further uses
            PreviousInputText = inputText;
            if (PreviousResults.ContainsKey(inputText.Length - 1))
            {
                PreviousResults[inputText.Length - 1] = results;
            } else
            {
                PreviousResults.Add(inputText.Length - 1, results);
            }

            return Sort(results, inputText).ToList();
        }

This is a method to make async search while typing, that's why there is this PreviousInputText and PreviousResults - to spare from superfluous queries

Movsar Bekaev
  • 862
  • 1
  • 11
  • 26
1

In the Threading section of the documentation, it says:

Persisted instances of Realm, RealmObject, IQueryable returned from Realm.All, or IList properties of RealmObjects can only be used on the thread on which they were created, otherwise an exception is thrown

In your code you're creating the Realm instance inside of Task.Run, but are trying to use the results after it returns. Instead of returning a list containing RealmObject instances, you can return a ThreadSafeReference to the query you're doing and resolve it on the original thread:

public async Task<IEnumerable<Entry>> FindAsync(string needle, IQueryable<Entry> haystack)
{
    var foregroundRealm = Realm.GetInstance();
    var haystackRef = haystack;
    var filteredEntriesRef = await Task.Run(() =>
    {
        using (var realm = Realm.GetInstance())
        {
            var bgHaystack = realm.ResolveReference(haystackRef);
            return ThreadSafeReference.Create(bgHaystack.Where(entry => entry.Content.ToLower().StartsWith(needle)));
        }
    });

    return foregroundRealm.ResolveReference(filteredEntriesRef).ToArray();
}
Nikola Irinchev
  • 1,562
  • 1
  • 12
  • 17
  • thanks for your help, seems like it should work but it doesn't, the app stops execution before reaching the return foregroundRealm.ResolveReference(filteredEntriesRef).ToArray(); - with no error – Movsar Bekaev May 05 '20 at 23:36
  • the execution stops at using (var realm = Realm.GetInstance()) line, please check if it works for you, along with shallow experience there is little info for realm with .net for me to inquire – Movsar Bekaev May 06 '20 at 00:09
0

Please replace your this line of code public async void Run() with the following

public async Task Run()

You should mark function async void only if it is fire and forget method, if you want to await on it, you should mark it as async Task, not async void.

Abdus Salam Azad
  • 3,109
  • 31
  • 23