username avatar
Kirti Kulkarni

April 22, 2022Beginner-12 min

5
12

Xamarin Offline data sync with Mongo Realm and RabbitMQ

In this article we are implementing the following scenario which involves :

  1. Net zero Xamarin mobile app
  2. Mongo Realm DB on the mobile device
  3. Realm Cloud DB
  4. RabbitMQ integration
Problem Statement
Xamarin based data Sync

We need the Xamarin based mobile app to work offline and for saving the data offline when there is no internet connection. The app then needs to sync data with the web when the connectivity is established.

We are using the Realm DB which offers an out of the box data sync feature that will help us to sync the offline data with the web.

The data also needs to be synced with the aspnetzero SQL database.

For this purpose, we are integrating RabbitMQ which triggers a data sync with the SQL database whenever any changes are pushed to Realm cloud.

Implementation

Now let us see how this scenario is implemented.

1. Create MongoDB account.

Get Started Free

2. Create New Organization or Select Existing Organization

Create New Organization or Select Existing Organization

3. Select Organization or Search for an Organization

Select Organization or Search for an Organization

4. Create New Project or Select Existing Project

Create New Project

5. Create New Database Deployments

Create New Database

6. New Database Deployments Created

New Database

7. Create Application in MongoDB Realm.

Create App In MongoDB

8. Add NuGet package ‘Realm’ to all Project Solutions.

Add NuGet Package

9. Add ‘appId’ in Xamarin File app.xaml.cs so it could interact with Application on MongoDB.

xamarinappXAML Code
private const string appId = "xamarin-xgdfn";
    public static Realms.Sync.App RealmApp;

10. Add Data in app.xaml.cs à ‘RealmApp = Realms.Sync.App.Create(appId);’.

Add Data
var appConfig = new AppConfiguration(appId)
    {
        //LogLevel = LogLevel.Debug,
        DefaultRequestTimeout = TimeSpan.FromMilliseconds(1500)
    };
    RealmApp = Realms.Sync.App.Create(appConfig);
    if (App.RealmApp.CurrentUser == null)
    {
        MainPage = new NavigationPage(new LoginPage());
    }
    else
    {
        DependencyService.Register();
    MainPage = new AppShell();
    }

11.Create Entries and Sync in xaml.cs files and added Realm Package file in them.

Create Entries Formcreate Entries JSCreate Entries Using

12. Add Objects and Users in Login and AddItems Page.

Add Objects and users in login and add items page
//On Top of the Page
    using AsyncTask = System.Threading.Tasks.Task; 
    // Inside class
   
   async void LoginButton_Clicked(object sender, EventArgs e)
   {
       await DoLogin();
   }
   
       private async AsyncTask DoLogin()
   {
       try
      {
   
       var user = await App.RealmApp.LogInAsync
   (Credentials.EmailPassword(email, password));
          if (user != null)
      {
          var projectPage = new AboutPage();
          await Navigation.PushAsync(projectPage);
      }
          else throw new Exception();
      }
          catch (Exception ex)
      {
          await DisplayAlert("Login Failed", ex.Message, "OK");
      }
   }

13. Create new Cluster and Integrated it with Application.

Database Deployments

14. Add Database and Collection in MongoDB Atlas à Cluster.

Add Database Collections

15. Add Models with ‘RealmObject’ and integrated them in xaml.cs files so the data could be passed through them.

Add Modelsclass DataFlow

16. Change Port ‘Server’ according to your device in Migrator and Host appsettings.json files

App Settings Json File

17. Run ‘dotnet run’ in Command Prompt of Migrator to migrate data.

18. After running the simulator, enter data in Entries and Sent data to Realm offline database.

Create New Tenant

19. After syncing the data is displayed on MongoDB Cluster à Database à Collection after it syncs online.

MongoDB Cluster

20. Create Worker Services(background services) project (Producer), which read data from MongoDB and send/publish the messages(data) to Asp.net Zero app (Consumers) using RabbitMQ

21. net Zero App(Consumer) received the message from producer and synch data into SQL server

22. RabbitMQ act a communicator middleware between both producer and consumers

Create Services (background services) project (Producer), which read data from MongoDB and send/ Worker publish the messages(data) to Asp.net Zero app (Consumers) using RabbitMQ

1. Create new Worker Service Project DbSyncWorker(background service)

Create New Project Db Sync Worker

2. Install Packages View - Other Windows - Package Manager Console

package Reference

3. Create Generic Repository class IRepository.cs class

Create Generic Repo Class
using System.Linq.Expressions;
  
   namespace Wai.DbSync.Interfaces
    {
       public interface IRepository : IDisposable where TEntity : class
      {
       void Add(TEntity obj);
       Task GetById(Guid id);
       Task<IEnumerable> GetAll();
       void Update(TEntity obj, MongoDB.Bson.ObjectId Id);
             
       void Remove(Guid id);
       Task<IEnumerable> Find(Expression<Func<TEntity, bool>> filter);
      }
    }   

4. Implement IRepository Interface

Implement IRepository Interface
 using MongoDB.Driver;
    using ServiceStack;
    using System.Linq.Expressions;
    using Wai.DbSync.Interfaces; 
            
    namespace Wai.DbSync.Repository
      
    {
        public abstract class BaseRepository 
    : IRepository where TEntity : class
      {
        protected readonly IMongoContext Context;
        protected IMongoCollection DbSet;
        
        protected BaseRepository(IMongoContext context)
      {
        Context = context;
        DbSet = Context.GetCollection(typeof(TEntity).Name);
      }
        
        public virtual void Add(TEntity obj)
      {
        Context.AddCommand(() => DbSet.InsertOneAsync(obj));
      }
        
        public virtual async Task GetById(Guid id)
      {
        var data = await DbSet.FindAsync(Builders.Filter.Eq("_id", id));
        return data.SingleOrDefault();
      }
        
        public virtual async Task<IEnumerable> GetAll()
      {
        var all = await DbSet.FindAsync(Builders.Filter.Empty);
        return all.ToList();
      }
        
        public virtual async Task<IEnumerable> 
    Find(Expression<Func<TEntity, bool>> filter)
      {
        var all = await DbSet.FindAsync(filter);
        return all.ToList();
      }
        
        public virtual void Update(TEntity obj, MongoDB.Bson.ObjectId Id)
      {         
        Context.AddCommand(() => DbSet.ReplaceOneAsync
    (Builders.Filter.Eq("_id", Id),obj));
        Context.SaveChanges();
        //Context.AddCommand(() => DbSet.ReplaceOneAsync
    (Builders.Filter.Eq("_id", obj.GetId()), obj));
      }
              
        
        public virtual void Remove(Guid id)
      {
        Context.AddCommand(() => DbSet.DeleteOneAsync
    (Builders.Filter.Eq("_id", id)));
      }
        
        public void Dispose()
      {
        Context?.Dispose();
      }
    
     }
     
    }

5. Create Model class

Create Modal Class
 using MongoDB.Bson;
     using MongoDB.Bson.Serialization.Attributes;
     using System.Text.Json.Serialization;
         
     namespace Wai.DbSync.Model
     {
         public record IntegrationEvent
       {        
         public IntegrationEvent()
       {
         Id = Guid.NewGuid();
         CreationDate = DateTime.UtcNow;
       }
         [JsonConstructor]
         public IntegrationEvent(Guid id, DateTime createDate)
       {
         Id = id;
         CreationDate = createDate;
       }
         [JsonInclude]
         public Guid Id { get; private init; 
       }
         [JsonInclude]
         public DateTime CreationDate { get; private init; }
       }
     }  
Integration Event Logs
using MongoDB.Bson;
    using MongoDB.Bson.Serialization.Attributes;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Text.Json;
        
    namespace Wai.DbSync.Model
    {
        public class IntegrationEventLogs
      {
        private IntegrationEventLogs() { }
        public IntegrationEventLogs(IntegrationEvent 
    @event, Guid transactionId)
      {    
        EventId = @event.Id;
        CreationTime = @event.CreationDate.ToLongDateString();
        EventTypeName = @event.GetType().FullName;
        Content = JsonSerializer.Serialize
    (@event, @event.GetType(), new JsonSerializerOptions
      {
        WriteIndented = true
      });
        State = EventStateEnum.NotPublished;
        TimesSent = 0;
        TransactionId = transactionId.ToString();               
      }       
        public byte[] Message { get; set; }
        public dynamic EventId { get; private set; }
        public string EventTypeName { get; private set; }
        [NotMapped]
        public string EventTypeShortName => EventTypeName.Split('.')?.Last();
        [NotMapped]
        public IntegrationEvent IntegrationEvent { get; private set; }
        public EventStateEnum State { get; set; }
        public int TimesSent { get; set; }
        public string CreationTime { get; private set; }
        public string Content { get; private set; }
        public string TransactionId { get; private set; }
        public IntegrationEventLogs DeserializeJsonContent(Type type)
      {
        IntegrationEvent = JsonSerializer.Deserialize
    (Content, type, new JsonSerializerOptions() 
    { PropertyNameCaseInsensitive = true }) as IntegrationEvent;
        return this;
      }
      }
        public enum EventStateEnum
      {
        NotPublished = 0,
        InProgress = 1,
        Published = 2,
        PublishedFailed = 3
      }
    }  

6. Implement Model

Wai Db Sync Repo
using Wai.DbSync.Model;
     namespace Wai.DbSync.Interfaces
      {
         public interface IIntegrationEventLogRepository : IRepository
       {
       
       }
     } 

7. Create Context class

Create Contect Class
using MongoDB.Driver;
    using Wai.DbSync.Interfaces;
        
    namespace Wai.DbSync.Context
    {
        public class MongoContext : IMongoContext
      {
        private IMongoDatabase Database { get; set; }
        public IClientSessionHandle Session { get; set; }
        public MongoClient MongoClient { get; set; }
        private readonly List<Func> _commands;
        private readonly IConfiguration _configuration;
        
        public MongoContext(IConfiguration configuration)
      {
        _configuration = configuration;
        
        // Every command will be stored and it'll be processed at SaveChanges
        _commands = new List<Func>();
      }   
        public async Task SaveChanges()
      {
        ConfigureMongo();
        try
      {
        using (Session = await MongoClient.StartSessionAsync())
      {    
        Session.StartTransaction();
        var commandTasks = _commands.Select(c => c());
        await Task.WhenAll(commandTasks);   
        await Session.CommitTransactionAsync();    
      }
      }
        catch (Exception e)
      {
      }
        return _commands.Count;
      }
        private void ConfigureMongo()
      {
        if (MongoClient != null)
      {
        return; 
      }
        // Configure mongo (You can inject the config, just to simplify)
        MongoClient = new MongoClient
    (_configuration["MongoSettings:Connection"]);
        Database = MongoClient.GetDatabase
    (_configuration["MongoSettings:DatabaseName"]);
      }
        public IMongoCollection GetCollection(string name)
      {
        ConfigureMongo();
        return Database.GetCollection(name);
      }
        public void Dispose()
      {
        Session?.Dispose();
        GC.SuppressFinalize(this);
      }
        public void AddCommand(Func func)
      {
        _commands.Add(func);
      }
      }
    }

8. UOM Implementation

UOM Implementation
using Wai.DbSync.Interfaces;
     namespace Wai.DbSync.UoW
     {
         public class UnitOfWork : IUnitOfWork
       {
         private readonly IMongoContext _context;
         public UnitOfWork(IMongoContext context)
       {
         _context = context;
       }  
         public async Task Commit()
       {
         var changeAmount = await _context.SaveChanges();
         return changeAmount > 0;
       } 
         public void Dispose()
       {
         _context.Dispose();
       }
       }
     } 

9. Configure appsetting.json

Configure Appsettings
{
      "Logging": 
    {
      "LogLevel": 
    {
      "Default": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    }
    },
      "MongoSettings": 
    {
      "Connection": "mongodb+srv://admin:123qwe@waicluster
  .rtj4h.mongodb.net/admin?retryWrites=true&w=majority?connect=replicaSet",
      "DatabaseName": "IntegrationEventLogs"	
    }
  } 
Configure Appsetting Class Worker

10. Create Worker.Cs class

Create Worker Class
using System.Text;
     using System.Text.Json;
     using MongoDB.Bson;
     using MongoDB.Driver;
     using RabbitMQ.Client;
     using Wai.DbSync.Interfaces;
         
     namespace DbSyncWorker;
         
     public class Worker : BackgroundService
     {
     private readonly ILogger _logger;
     private readonly IIntegrationEventLogRepository 
     _integrationEventLogRepository;
     private readonly IUnitOfWork _unitOfWork;
     public Worker(ILogger logger, 
     IIntegrationEventLogRepository 
     integrationEventLogRepository, IUnitOfWork unitOfWork)
     {
     _logger = logger;
     _integrationEventLogRepository = integrationEventLogRepository;
     _unitOfWork = unitOfWork;
     }
         
     protected override async Task ExecuteAsync
     (CancellationToken stoppingToken)
     {
     while (!stoppingToken.IsCancellationRequested)
     {      
         
     var docs = await _integrationEventLogRepository
     .Find(x => x.State == 
     Wai.DbSync.Model.EventStateEnum.NotPublished);      
                 
     var factory = new ConnectionFactory() { HostName = "localhost" };
         
     using (var connection = factory.CreateConnection())
     using (var channel = connection.CreateModel())
     {
     channel.QueueDeclare(queue: "hello",
     durable: false,
     exclusive: false,
     autoDelete: false,
     arguments: null);
     var properties = channel.CreateBasicProperties();
     properties.Persistent = false;
     foreach (var doc in docs)
     {
     if (doc.Message is null)
     {
         continue;
     }
     channel.BasicPublish(exchange: string.Empty,
     routingKey: "hello",
     basicProperties: null,
     body: doc.Message);
     doc.State = Wai.DbSync.Model.EventStateEnum.InProgress;
     _integrationEventLogRepository.Update(doc, doc.EventId);
                       
     Console.WriteLine(" [x] Sent {0}", docs.FirstOrDefault()?.Message);
     } 
     }
     await Task.Delay(5000, stoppingToken);
     }
     }
     }

11. Add a synch method ExecuteAsync which is call after every 5 second

Add ASync Method

12. Configure Service in Program.cs

Configure Service
using DbSyncWorker;
     using Wai.DbSync.Context;
     using Wai.DbSync.Interfaces;
     using Wai.DbSync.Persistence;
     using Wai.DbSync.Repository;
     using Wai.DbSync.UoW;
         
     IHost host = Host.CreateDefaultBuilder(args)
     .ConfigureServices(services =>
     {
     MongoDbPersistence.Configure();
     services.AddTransient<IMongoContext, MongoContext>();
     services.AddTransient<IUnitOfWork, UnitOfWork>();
     services.AddTransient<IIntegrationEventLogRepository, 
     IntegrationEventLogRepository>();
     services.AddHostedService();
     })
     .Build();
     await host.RunAsync();
Performing Changes in the ASP.Net zero Application. This application received message from producer and synch data into SQL server

First create a new ASP.Net Zero application either using following the steps outlined here

Getting Started Angular | Documentation Center (aspnetzero.com)

Do changes as per below steps

1. Add package RabbitMQ in asp.net project

Add Package

2. Add New Class MessageHandler.cs in Core project

Add Class Message Handler

3.Add method DoWork in MessageHandler.cs class: Periodic works should be done by implementing this method

Do Work Method
using System;
     using System.Collections.Generic;
     using System.Linq;
     using System.Text;
     using System.Threading.Tasks;
     using Abp.Dependency;
     using Abp.Domain.Repositories;
     using Abp.Domain.Uow;
     using Abp.Threading.BackgroundWorkers;
     using Abp.Threading.Timers;
     using MyTraining1110Demo.Guitars;
     using Newtonsoft.Json;
     using RabbitMQ.Client;
     using RabbitMQ.Client.Events;
     namespace MyTraining1110Demo.RabbitMq
     {
     public class MessageHandler : 
     PeriodicBackgroundWorkerBase, ISingletonDependency
     {
     private readonly IRepository _repository;
     private readonly IUnitOfWorkManager _unitOfWorkManager;
     public bool flg = false;
     public MessageHandler
     (AbpTimer timer, IRepository 
     repository, IUnitOfWorkManager unitOfWorkManager) : base(timer)
     {
     Timer.Period = 10000; //5 seconds 
     (good for tests, but normally will be more)
     _repository = repository;
     _unitOfWorkManager = unitOfWorkManager;
     }
     [UnitOfWork]
     protected override void DoWork()
     {
     var factory = new ConnectionFactory() 
     { HostName = "localhost", DispatchConsumersAsync = true };
     using (var connection = factory.CreateConnection())
     using (var channel = connection.CreateModel())
     {
     channel.QueueDeclare(queue: "hello",
     durable: true,
     exclusive: false,
     autoDelete: false,
     arguments: null);
     var consumer = new AsyncEventingBasicConsumer(channel);
     consumer.Received += async (model, ea) =>
     {
     using (var uow = _unitOfWorkManager.Begin
     (System.Transactions.TransactionScopeOption.RequiresNew))
     {
     try
     {
     var body = ea.Body.ToArray();
     var message = Encoding.UTF8.GetString(body);
     var guitar = JsonConvert.DeserializeObject(message);
     var record = await _repository.InsertAsync(guitar);
     }
     catch (Exception ex)
     {
     }
     await uow.CompleteAsync();
     }
     };
     channel.BasicConsume(queue: "hello",
     autoAck: true,
     consumer: consumer);	
     connection.Close();
     }
     }
     }} 

4. Add Entry in (ProjectName)Module.cs class of Core project

Post Initialize Method
var workManager = IocManager.Resolve();
     workManager.Add(IocManager.Resolve());

5.Changes in MyTraining1101DemoWebHostModule.cs of WebHost Project as

Changes In MyTraining
 workManager.Add(IocManager.Resolve()); 
Featured Comments
username avatar
Joe ThomsonToday at 5:42PM

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor

username avatar
Joe ThomsonToday at 5:42PM

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor

username avatar
Joe ThomsonToday at 5:42PM

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor

username avatar
Kirti Kulkarni

ABOUT THE AUTHOR

With over 20 years of experience in software development, Kirti heads Product R&D and Competency Management at WAi Technologies, leading the training and skills upgradation program at WAi. Kirti introduced the 'Women Back To Work' Initiative that encourages women to return back to mainstream software development after a career break or sabbatical.