Is there a bare-bone C# example for Mobile / Sync-Gateway?

The problem I’m having is that there are a lot of moving parts in the examples and it’s very hard to see what comes from where. Is there a tutorial without IoC, MVVM and the split into several different projects?

I.e. just bare-bones, the minimum needed to achieve a connection, sync and successful queries.

Okay, tried to make my own. Only to hit a snag and I’m not sure where it’s coming from because it simply fails silently.

So I have a simple DatabaseService like this (yes, I know, it’s not pretty but …)

public sealed class DatabaseService : IDisposable
    {
        readonly Uri _remoteSyncUrl = new Uri("ws://192.168.2.41:4984");
        readonly string _databaseName = "defaultdb";
        Replicator _replicator;
        ListenerToken _replicatorListenerToken;
        Database _database;

        public DatabaseService() { 
                
        }

        public Database GetDatabase()
        {
            if (_database == null)
            {
                var options = new DatabaseConfiguration();
                var directory = FileSystem.Current.AppDataDirectory;

                _database = new Database(_databaseName, options);
            }
            return _database;
        }

        public void StartReplicationAsync(string username, string password, string[] channels, ReplicatorType replicationType = ReplicatorType.PushAndPull, bool continuous = true)
        {
            var database = GetDatabase();
            var targetEndpointUrl = new URLEndpoint(new Uri(_remoteSyncUrl, _databaseName));
            var configuration = new ReplicatorConfiguration(_database, targetEndpointUrl)
            {
                ReplicatorType = replicationType,
                Continuous = continuous,
                Authenticator = new BasicAuthenticator(username, password),
                Channels = channels?.Select(x => $"channel.{x}").ToArray()
            };
            _replicator = new Replicator(configuration);
            _replicatorListenerToken = _replicator.AddChangeListener(OnReplicatorUpdate);
            _replicator.Start();
            
        }

        void OnReplicatorUpdate(object sender, ReplicatorStatusChangedEventArgs e)
        {
            var stat = _replicator.Status;
            var status = e.Status;

            switch (status.Activity)
            {
                case ReplicatorActivityLevel.Busy:
                    Console.WriteLine("Busy transferring data.");
                    break;
                case ReplicatorActivityLevel.Connecting:
                    Console.WriteLine("Connecting to Sync Gateway.");
                    break;
                case ReplicatorActivityLevel.Idle:
                    Console.WriteLine("Replicator in idle state.");
                    break;
                case ReplicatorActivityLevel.Offline:
                    Console.WriteLine("Replicator in offline state.");
                    break;
                case ReplicatorActivityLevel.Stopped:
                    Console.WriteLine("Completed syncing documents.");
                    break;
            }

            if (status.Progress.Completed == status.Progress.Total)
            {
                Console.WriteLine("All documents synced.");
            }
            else
            {
                Console.WriteLine($"Documents {status.Progress.Total - status.Progress.Completed} still pending sync");
            }
        }

        public void Dispose()
        {
            if(_replicator != null )
            {
                StopReplication();
                while(true)
                {
                    if (_replicator.Status.Activity == ReplicatorActivityLevel.Stopped)
                    {
                        break;
                    }
                }
                _replicator.Dispose();
            }
        }
        public void StopReplication()
        {
            _replicator.RemoveChangeListener(_replicatorListenerToken);
            _replicator.Stop();
        }
    }

And then I try to save a document somewhere else like this:

int count = 0;
[...]
count++;
var database = _testRepository.GetDatabase();
var mutableDocument = new MutableDocument("foo");
mutableDocument.SetInt("count", count);
database.Save(mutableDocument);

Now, on a local level this does save to the local db. I can retrieve the document with the correct value just fine. But this does not get propagated through the SyncGateway to the remote database.

There are no errors. If I set breakpoints in the OnReplicatorUpdate method then I see that the method does get hit (3 times to be exact).
The weird thing is that _replicator.Status changes from idle to busy and back again to idle, while e.Status stays at stopped the whole time.

To make matters worse, this code worked once. After that, it decided not to replicate the document anymore.

Found the problem - it was located somewhere completely different. I would maybe advise adding this to the docs somewhere in a prominent location.

You see, in order to observe changes on the server, I made use of Couchbases own Admin panel (i.e. dashboard and the “Documents” tab). And since I wanted to see if changes made there replicate back to the clients, I directly edited a document there.

This breaks the replication completely, utterly and with neither a warning nor an error.

The only way to fix this (at least according to my experiments) is to delete the database on the client, delete the document and also delete the sync:rev:doc_id documents.

Basically, the ability to edit documents directly on the server is very dangerous.

Just editing the docs. on the server doesn’t break anything - I do that now and again… as well as have a server based application using the Java SDK update the database.

So it must be something else. How have you configured the sync. gateway? They’re log files on the sync. gateway that you could look at to check any errors. I would suggest this as the first place to look.

This is what appears in the logs when I increment the counter from my example by 1:

Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.256Z [DBG] WSFrame+: c:#001 Received frame: MSG#11!~ (flags=   11000, length=45)
Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.256Z [DBG] WS+: c:#001 Incoming BLIP Request: MSG#11!~
Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.256Z [INF] SyncMsg: c:[28982a61] #11: Type:proposeChanges #Changes: 1
Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.256Z [DBG] SyncMsg+: c:[28982a61] #11: Type:proposeChanges   --> OK Time:544.663<C2><B5>s
Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.256Z [DBG] WSFrame+: c:#001 Push RPY#11!~
Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.256Z [DBG] WSFrame+: c:#001 Sending frame: RPY#11!~ (flags=   11001, size=   61)
Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.308Z [DBG] WSFrame+: c:#001 Received frame: MSG#12 (flags=       0, length=101)
Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.308Z [DBG] WS+: c:#001 Incoming BLIP Request: MSG#12
Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.308Z [INF] SyncMsg: c:[28982a61] #12: Type:setCheckpoint Client:cp-cwUKpGBE9x+Deja65JF8Vvi1MSY= Rev:0-16
Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.309Z [DBG] SyncMsg+: c:[28982a61] #12: Type:setCheckpoint   --> OK Time:848.294<C2><B5>s
Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.309Z [DBG] WSFrame+: c:#001 Push RPY#12
Nov 16 21:08:32 couchbase bash[7703]: 2022-11-16T21:08:32.309Z [DBG] WSFrame+: c:#001 Sending frame: RPY#12 (flags=       1, size=   10)

This is the config for the gateway:

{
  "bootstrap": {
    "server": "couchbase://localhost",
    "username": "sync_gateway",
    "password": "foobarbaz",
    "server_tls_skip_verify": true,
    "use_tls_server": false
  },
  "logging": {
    "console": {
      "enabled": true,
      "log_level": "debug",
      "log_keys": ["*"]
    }
  },
  "api": {
    "admin_interface": "192.168.2.41:4985"
  }
}

The bucket’s config is set to:

{
    "bucket": "default",
    "name": "defaultdb",
    "import_docs": true,
    "enable_shared_bucket_access": true,
    "num_index_replicas": 0
}

Hmmh, I reconfigured the logs a bit and found this on the info level:

2022-11-17T19:54:55.021Z [INF] Changes: c:[5964a8e9] Channels [ <ud>channel.public</ud> ] request without access by user <ud>sgwuser1</ud>
2022-11-17T19:54:55.021Z [INF] Sync: c:[5964a8e9] Sent all changes to client

Now I’m confused. Isn’t the public channel supposed to be available to all by default? And, according to a call to /db/_user/sgwuser1, said user should have access?

{
    "name": "sgwuser1",
    "admin_channels": [
        "public"
    ],
    "all_channels": [
        "!",
        "public"
    ],
    "disabled": false
}

The logs are informing you that you’re trying to replicate a channel named “channel.public”, which is different than the two channels the user has access to (“public” and “!”).

Nor sure where the ! is coming from. Well, I changed the channel to public anywhere I could find (highlights the dangers of copy&pasting code without (yet) fully understanding it).

That didn’t help.

But pointing at the channel helped me figure out where the problem was coming from. Namely that I was using the default sync function which assigns channels to a document based on its channels property.
Which I did not set. Which meant that the document did not belong to any channel. Once I set that property, everything worked as it should.

Of course, with the channels attributes set by the client itself, I now have to figure out how to disallow setting this to a channel the client does not have access to. Because currently anyone can write anywhere :slight_smile:

Oh, and one more thing: I stumbled across the XATTRS-attribute. When I tried to enable that, the server told me that it’s Enterprise only. Didn’t see that mentioned in the docs, however.

You can see the differences between Enterprise edition and Community edition here https://www.couchbase.com/products/editions
The doc page on channels might also be of interest Channels | Couchbase Docs

Well, turns out that XATTRS is useless for my case anyway because I need much more finegrained controls.

For example, making sure that a new document is assigned to a channel the user actually has access to - since there doesn’t seem to exist a user object in the sync function, I don’t see how that would work. Or making one particular channel read-only, except for some persons (not roles!). Or allowing edits for only x minutes after document creation (where x is a changeable configuration value).

I guess I’ll go for a “pullOnly” configuration, thus set the sync function to “read only” and then create new documents through REST/GPRC/SignalR calls directly on the server. Not a problem for text only but binary objects like photos / videos thus incur higher bandwidth use because they’ll first go downstream and then upstream again.