Search code examples
c#.netasynchronousasync-awaitrealm

Realm database asynchronous queries and .Net


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 "";
            }
        }
    }
}

Solution

  • 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