![username avatar](https://t4.ftcdn.net/jpg/04/10/43/77/240_F_410437733_hdq4Q3QOH9uwh0mcqAhRFzOKfrCR24Ta.jpg)
April 22, 2022Beginner-12 min
Xamarin Offline data sync with Mongo Realm and RabbitMQ
In this article we are implementing the following scenario which involves :
- Net zero Xamarin mobile app
- Mongo Realm DB on the mobile device
- Realm Cloud DB
- RabbitMQ integration
Problem Statement
![Xamarin based data Sync](/static/dataSync-0a24acf09368e2e972b8fbeaf51b6970.png)
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](/static/getStartedFree-8c7efd79b2c14ae7569722d04c5394ce.png)
2. Create New Organization or Select Existing Organization
![Create New Organization or Select Existing Organization](/static/organizations-79c0228091ffd4ebe17a44690ee9ff2a.png)
3. Select Organization or Search for an Organization
![Select Organization or Search for an Organization](/static/selectOrganizations-15488959570fded3e140b821be0f15b1.png)
4. Create New Project or Select Existing Project
![Create New Project](/static/createNewProject-cad9ff9fad925e4fdbd4c65fed473ba6.png)
5. Create New Database Deployments
![Create New Database](/static/CreateNewDatabase-70b16ebf40a3312431282d8e6f882baa.png)
6. New Database Deployments Created
![New Database](/static/newDatabase-ff7e02556dbe785866f9889a4a9a2736.png)
7. Create Application in MongoDB Realm.
![Create App In MongoDB](/static/createAppInMongoDB-6dbd51d7c92623ccfe07264fdf29b92c.png)
8. Add NuGet package ‘Realm’ to all Project Solutions.
![Add NuGet Package](/static/addNuGetPackage-c77ee0418941ffa05a8a4eaaa17b2839.png)
9. Add ‘appId’ in Xamarin File app.xaml.cs so it could interact with Application on MongoDB.
![xamarin](/static/xamarin-3c124a5ea42e3a3e1a644d8b4afbf24e.png)
![appXAML Code](/static/appXAML-6cfa226cd803098df7f1d56c45ef7f38.png)
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](/static/addData-91b99dedc7c9729229c3fab7bd3ed3ca.png)
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 Form](/static/createEntriesForm-692fdb19738696afce5ce08d6ab82d33.png)
![create Entries JS](/static/createEntriesJS-511f925d8aaec564f1dcb507fab0419e.png)
![Create Entries Using](/static/createEntriesUsing-d488116d5159f6e72fda16c9e00272f3.png)
12. Add Objects and Users in Login and AddItems Page.
![Add Objects and users in login and add items page](/static/addObjectsUserLogin-f41fbc718666eb9b9cc5344a45c04568.png)
//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](/static/databaseDeployments-dff000f4beae88187d8d026b072f1abd.png)
14. Add Database and Collection in MongoDB Atlas à Cluster.
![Add Database Collections](/static/databaseCollections-5e163ae8f216e2987adf142292c1e532.png)
15. Add Models with ‘RealmObject’ and integrated them in xaml.cs files so the data could be passed through them.
![Add Models](/static/addModels-6cc23642d66df1eac1d49901cdf7b52f.png)
![class DataFlow](/static/classDataFlow-a0a0eb268ddad12b03bb74c9a3b22fba.png)
16. Change Port ‘Server’ according to your device in Migrator and Host appsettings.json files
![App Settings Json File](/static/appSettingsJson-6cb471815becb0211965eb4f3fa3937a.png)
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](/static/createNewTenant-b94d7235d0a6903f952ee501ca0271cd.png)
19. After syncing the data is displayed on MongoDB Cluster à Database à Collection after it syncs online.
![MongoDB Cluster](/static/mongoDBCluster-4d1439e6286e301fdfcedddc07f892e9.png)
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](/static/createNewProjDbSyncWorker-e72497a76e0ddcf51f17b574244b49ec.jpg)
2. Install Packages View - Other Windows - Package Manager Console
![package Reference](/static/packageReference-1133b735ca19a6998ab5560fea955cb3.jpg)
3. Create Generic Repository class IRepository.cs class
![Create Generic Repo Class](/static/createGenericRepoClass-4e743ead4b68e4e1246e32a687a93545.jpg)
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](/static/implementIRepoInterface-d566466358498c4114e76a70d6941f1a.jpg)
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](/static/createModalClass-79b28ba16bb0c97e1f9282b729389355.jpg)
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](/static/integrationEventLogs-6459e15f51784a5bc79594151a5a98b4.jpg)
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](/static/waiDbSyncRepo-3162bd59bb94c94ded6ad6f1607f674b.jpg)
using Wai.DbSync.Model;
namespace Wai.DbSync.Interfaces
{
public interface IIntegrationEventLogRepository : IRepository
{
}
}
7. Create Context class
![Create Contect Class](/static/createContectClass-9f75d90f0fb837fcfac07309885d647b.jpg)
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](/static/UOMImplementation-11090b1a7e919cd09534820b5227545b.jpg)
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](/static/configureAppsettings-e514248e3bb2875baaad642b8db7fbda.jpg)
{
"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](/static/configureAppsettingClassWorker-1203c2ebecd11897df96036f38035025.jpg)
10. Create Worker.Cs class
![Create Worker Class](/static/createWorkerClass-1203c2ebecd11897df96036f38035025.jpg)
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](/static/addASyncMethod-f819de1533b46fd0e9de292b02967193.jpg)
12. Configure Service in Program.cs
![Configure Service](/static/configureService-5d48bbab0b4af467450385ad72629b13.jpg)
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
2. Add New Class MessageHandler.cs in Core project
![Add Class Message Handler](/static/addClassMessageHandler-06e9c77f4009d73058c41bc761f683be.jpg)
3.Add method DoWork in MessageHandler.cs class: Periodic works should be done by implementing this method
![Do Work Method](/static/doWorkMethod-cc490e594187ccbb957c14b9dc43d2a6.jpg)
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](/static/postInitializeMethod-d04e8100646acd2610d3d2b83e885284.jpg)
var workManager = IocManager.Resolve();
workManager.Add(IocManager.Resolve());
5.Changes in MyTraining1101DemoWebHostModule.cs of WebHost Project as
![Changes In MyTraining](/static/changesInMyTraining-0314a61964aa3ad1f7adb48ce9646034.jpg)
workManager.Add(IocManager.Resolve());
Featured Comments
![username avatar](https://t4.ftcdn.net/jpg/04/10/43/77/240_F_410437733_hdq4Q3QOH9uwh0mcqAhRFzOKfrCR24Ta.jpg)
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
![username avatar](https://t4.ftcdn.net/jpg/04/10/43/77/240_F_410437733_hdq4Q3QOH9uwh0mcqAhRFzOKfrCR24Ta.jpg)
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
![username avatar](https://t4.ftcdn.net/jpg/04/10/43/77/240_F_410437733_hdq4Q3QOH9uwh0mcqAhRFzOKfrCR24Ta.jpg)
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
More Articles from Kirti Kulkarni
![username avatar](https://t4.ftcdn.net/jpg/04/10/43/77/240_F_410437733_hdq4Q3QOH9uwh0mcqAhRFzOKfrCR24Ta.jpg)
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.