In the Runtime .NET API, I found a method named ImportGeodatabaseDeltaAsync.
Unfortunately, there is no documentation how to use this method in a full workflow. So I have to try many things.
The first parameter is
geodatabaseFileName
Type: System.String
The path and filename of geodatabase where delta is applied to.
It seems that it has to be a Runtime geodatabase. I could use my Runtime geodatabase anyway, which I have created with my Runtime client against an sync enabled ArcGIS Server (FeatureServer) service. So, this is no problem, as I could create it with the Runtime SDK method GenerateGeodatabase.
The second parameter is
deltaGeodatabaseFileName
Type: System.String
The path and filename of geodatabase where to import the delta from
This one could not be a normal Runtime geodatabase file, because you will get an exception in that case:
Esri.ArcGISRuntime.ArcGISRuntimeException: 'SQL error or missing database: no such table: GDB_DataChangesDatasets'
Obiviously the geodatase has a different structure.
I could not find any SDK functionality for creating a delta package, so I tried to create one directly from the REST API:
We created a replica first, added a new feature in the service and then called the synchronizeReplica operation. The result was a geodatabase file, which now has different schema as there exists some additional tables in it.
When using this geodatabase for the deltaGeodatabaseFileName Parameter, the ImportGeodatabaseDeltaAsync works.
My question now is:
And maybe adding a documentation for this workflow would be helpful, as I could not find anything about it.
Hi Maximilian Glas,
Probably too late for this, but check this out:
http://desktop.arcgis.com/en/arcmap/10.3/tools/data-management-toolbox/export-to-delta.htm
It creates a delta using a child replica geodatabase.
Also not sure if this is still a concern.
The way you generate the delta is with the rest API https://developers.arcgis.com/rest/services-reference/synchronize-replica.htm. You could do through python or in C# with HttpClient.
This is some code that does in C#, most of everything is here, excluding some custom objects that define some of the basics which I think can be figured out.
//returns the Url of the delta file which is used by the calling method to download
public async Task<string> SyncronizeReplicaAsync(ReplicaDefinition replicaDefinition)
{
try
{
ArcGISHttpClientHandler handler = new ArcGISHttpClientHandler { ArcGISCredential = await _portalManager.Credential(true) };
using ( var client = new HttpClient(handler) )
{
try
{
var featureServiceUrl = new Uri($"{FeatureServiceUrl(replicaDefinition)}?f=json");
var get = await client.GetStringAsync(featureServiceUrl);
}
catch (Exception)
{
_log.Warn($"Could not connect to Url: {FeatureServiceUrl(replicaDefinition)}");
return null;
}
//This gets the replica url wich is featureserviceUrl/replicaId
var requestUri = new Uri($"{FeatureServiceUrl(replicaDefinition)}/synchronizeReplica");
var parameters = new Dictionary<string, string>
{
{"f", "json"},
{"async", "true"},
{"dataFormat", "sqlite"},
{"replicaID", replicaDefinition.ReplicaId.ToLower()},
{"rollbackOnFailure", "false"},
{"syncLayers", await GetSyncLayersAsync(replicaDefinition)},
{"transportType", "esriTransportTypeUrl"}
};
var content = new FormUrlEncodedContent(parameters);
//This sends off the sync request to the server
var response = await client.PostAsync(requestUri, content);
if ( !(response.Content is ByteArrayContent byteArrayContent) ) return null;
var json = await byteArrayContent.ReadAsStringAsync();
var status = JsonConvert.DeserializeObject<StatusResponse>(json);
var resultUrl = await CheckJobAsync(replicaDefinition, status);
return resultUrl;
}
}
catch (Exception e)
{
_log.Error(e.Message, e);
return null;
}
}
// I have some custom objects that mimic responses and use json deserializer to convert to .net objects
private async Task<string> CheckJobAsync(ReplicaDefinition replicaDefinition, StatusResponse status)
{
JobResponse job = null;
ArcGISHttpClientHandler handler = null;
try
{
while ( true )
{
handler = new ArcGISHttpClientHandler {ArcGISCredential = await _portalManager.Credential(false)};
using (var client = new HttpClient(handler))
{
//This is a periodic check of the Job status
var requestUri = new Uri($"{status.StatusUrl}?f=json");
var json = await client.GetStringAsync(requestUri);
job = JsonConvert.DeserializeObject<JobResponse>(json);
// When job is complete the respence incluses the Url of the delta database
if ( job.JobStatus == JobStatus.Completed ) break;
if ( job.JobStatus == JobStatus.Failed )
{
_log.Warn($"Job failed: {replicaDefinition.Name}");
return null;
}
if ( job.JobStatus == JobStatus.Other )
{
_log.Warn($"Job unknown: {replicaDefinition.Name}");
return null;
}
await Task.Delay(TimeSpan.FromSeconds(5));
}
}
}
catch (ArcGISWebException e)
{
if ( handler?.ArcGISCredential is TokenCredential tokenCredential )
{
_log.Error($"{e.Message}: {tokenCredential.ExpirationDate?.LocalDateTime:hh:mm:ss}");
}
else
{
_log.Error(e.Message, e);
}
}
catch (Exception e)
{
_log.Error(e.Message, e);
}
return job?.ResultUrl;
}
// Gets the layer array required by the syncronizeReplica request
private async Task<string> GetSyncLayersAsync(ReplicaDefinition replicaDefinition)
{
try
{
var handler = new ArcGISHttpClientHandler { ArcGISCredential = await _portalManager.Credential(false) };
using ( var client = new HttpClient(handler){Timeout = TimeSpan.FromMinutes(5)} )
{
var requestUri = new Uri($"{FeatureServiceUrl(replicaDefinition)}/replicas/{replicaDefinition.ReplicaId}?f=json");
var response = await client.GetStringAsync(requestUri);
var jObject = JObject.Parse(response);
var syncLayers = JsonConvert.DeserializeObject<IEnumerable<SyncLayer>>(jObject["layerServerGens"].ToString()).ToArray();
foreach (var syncLayer in syncLayers)
{
syncLayer.SyncDirection = "download";
}
//Setting to allow to sync back beyond last sync time
// The serverGen parameter is actually the time stamp that the syncronizeReplica looks back into the past
// by default this is retrieved from the replica definition.
//This is logic to allow us to push that time back in case we need to go further to the past
if (_appSettings.DownloadSyncBack)
{
long time = (DateTimeOffset.UtcNow - TimeSpan.FromDays(_appSettings.DownloadDaysBack)).ToUnixTimeMilliseconds();
foreach (var syncLayer in syncLayers)
{
syncLayer.ServerGen = time;
}
}
var serializerSettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
string json = JsonConvert.SerializeObject(syncLayers, serializerSettings);
return json;
}
}
catch (Exception e)
{
_log.Error(e.Message, e);
return null;
}
}