summaryrefslogtreecommitdiff
path: root/dsa/FireBase
diff options
context:
space:
mode:
authorDennis Kobert <d-kobert@web.de>2019-06-11 23:38:13 +0200
committerDennis Kobert <d-kobert@web.de>2019-06-11 23:38:13 +0200
commit2fa4a0e50ebfc97059c8b84dbd17e79f9afc8a8d (patch)
treec3b34ccb2737e347a73768536895cbbaab13cc01 /dsa/FireBase
parentec991104f56e90d7bb2878da2fe6ed4e585dfc46 (diff)
parentaf74efccf8d21e6151022b71f3cacd3fa83024ee (diff)
Merge branch 'rework-backend'
Diffstat (limited to 'dsa/FireBase')
-rw-r--r--dsa/FireBase/ExceptionEventArgs.cs28
-rw-r--r--dsa/FireBase/Extensions/ObservableExtensions.cs41
-rw-r--r--dsa/FireBase/Extensions/TaskExtensions.cs23
-rw-r--r--dsa/FireBase/FireBase.csproj13
-rw-r--r--dsa/FireBase/FirebaseClient.cs49
-rw-r--r--dsa/FireBase/FirebaseException.cs53
-rw-r--r--dsa/FireBase/FirebaseKeyGenerator.cs79
-rw-r--r--dsa/FireBase/FirebaseObject.cs27
-rw-r--r--dsa/FireBase/FirebaseOptions.cs52
-rw-r--r--dsa/FireBase/Http/HttpClientExtensions.cs123
-rw-r--r--dsa/FireBase/Http/PostResult.cs13
-rw-r--r--dsa/FireBase/ObservableExtensions.cs40
-rw-r--r--dsa/FireBase/Offline/ConcurrentOfflineDatabase.cs233
-rw-r--r--dsa/FireBase/Offline/DatabaseExtensions.cs257
-rw-r--r--dsa/FireBase/Offline/ISetHandler.cs10
-rw-r--r--dsa/FireBase/Offline/InitialPullStrategy.cs23
-rw-r--r--dsa/FireBase/Offline/Internals/MemberAccessVisitor.cs46
-rw-r--r--dsa/FireBase/Offline/OfflineCacheAdapter.cs152
-rw-r--r--dsa/FireBase/Offline/OfflineDatabase.cs228
-rw-r--r--dsa/FireBase/Offline/OfflineEntry.cs99
-rw-r--r--dsa/FireBase/Offline/RealtimeDatabase.cs479
-rw-r--r--dsa/FireBase/Offline/SetHandler.cs19
-rw-r--r--dsa/FireBase/Offline/StreamingOptions.cs23
-rw-r--r--dsa/FireBase/Offline/SyncOptions.cs28
-rw-r--r--dsa/FireBase/Query/AuthQuery.cs34
-rw-r--r--dsa/FireBase/Query/ChildQuery.cs50
-rw-r--r--dsa/FireBase/Query/FilterQuery.cs77
-rw-r--r--dsa/FireBase/Query/FirebaseQuery.cs314
-rw-r--r--dsa/FireBase/Query/IFirebaseQuery.cs40
-rw-r--r--dsa/FireBase/Query/OrderQuery.cs34
-rw-r--r--dsa/FireBase/Query/ParameterQuery.cs43
-rw-r--r--dsa/FireBase/Query/QueryExtensions.cs210
-rw-r--r--dsa/FireBase/Query/QueryFactoryExtensions.cs187
-rw-r--r--dsa/FireBase/Query/SilentQuery.cs18
-rw-r--r--dsa/FireBase/Settings.StyleCop77
-rw-r--r--dsa/FireBase/Streaming/FirebaseCache.cs181
-rw-r--r--dsa/FireBase/Streaming/FirebaseEvent.cs37
-rw-r--r--dsa/FireBase/Streaming/FirebaseEventSource.cs38
-rw-r--r--dsa/FireBase/Streaming/FirebaseEventType.cs18
-rw-r--r--dsa/FireBase/Streaming/FirebaseServerEventType.cs15
-rw-r--r--dsa/FireBase/Streaming/FirebaseSubscription.cs217
-rw-r--r--dsa/FireBase/Streaming/NonBlockingStreamReader.cs63
42 files changed, 3791 insertions, 0 deletions
diff --git a/dsa/FireBase/ExceptionEventArgs.cs b/dsa/FireBase/ExceptionEventArgs.cs
new file mode 100644
index 0000000..09c205a
--- /dev/null
+++ b/dsa/FireBase/ExceptionEventArgs.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace Firebase.Database
+{
+ /// <summary>
+ /// Event args holding the <see cref="Exception" /> object.
+ /// </summary>
+ public class ExceptionEventArgs<T> : EventArgs where T : Exception
+ {
+ public readonly T Exception;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExceptionEventArgs" /> class.
+ /// </summary>
+ /// <param name="exception"> The exception. </param>
+ public ExceptionEventArgs(T exception)
+ {
+ Exception = exception;
+ }
+ }
+
+ public class ExceptionEventArgs : ExceptionEventArgs<Exception>
+ {
+ public ExceptionEventArgs(Exception exception) : base(exception)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Extensions/ObservableExtensions.cs b/dsa/FireBase/Extensions/ObservableExtensions.cs
new file mode 100644
index 0000000..0a672d7
--- /dev/null
+++ b/dsa/FireBase/Extensions/ObservableExtensions.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Reactive.Linq;
+
+namespace Firebase.Database.Extensions
+{
+ public static class ObservableExtensions
+ {
+ /// <summary>
+ /// Returns a cold observable which retries (re-subscribes to) the source observable on error until it successfully
+ /// terminates.
+ /// </summary>
+ /// <param name="source">The source observable.</param>
+ /// <param name="dueTime">How long to wait between attempts.</param>
+ /// <param name="retryOnError">A predicate determining for which exceptions to retry. Defaults to all</param>
+ /// <returns>
+ /// A cold observable which retries (re-subscribes to) the source observable on error up to the
+ /// specified number of times or until it successfully terminates.
+ /// </returns>
+ public static IObservable<T> RetryAfterDelay<T, TException>(
+ this IObservable<T> source,
+ TimeSpan dueTime,
+ Func<TException, bool> retryOnError)
+ where TException : Exception
+ {
+ var attempt = 0;
+
+ return Observable.Defer(() =>
+ {
+ return (++attempt == 1 ? source : source.DelaySubscription(dueTime))
+ .Select(item => new Tuple<bool, T, Exception>(true, item, null))
+ .Catch<Tuple<bool, T, Exception>, TException>(e => retryOnError(e)
+ ? Observable.Throw<Tuple<bool, T, Exception>>(e)
+ : Observable.Return(new Tuple<bool, T, Exception>(false, default(T), e)));
+ })
+ .Retry()
+ .SelectMany(t => t.Item1
+ ? Observable.Return(t.Item2)
+ : Observable.Throw<T>(t.Item3));
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Extensions/TaskExtensions.cs b/dsa/FireBase/Extensions/TaskExtensions.cs
new file mode 100644
index 0000000..c955b3a
--- /dev/null
+++ b/dsa/FireBase/Extensions/TaskExtensions.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Firebase.Database.Extensions
+{
+ public static class TaskExtensions
+ {
+ /// <summary>
+ /// Instead of unwrapping <see cref="AggregateException" /> it throws it as it is.
+ /// </summary>
+ public static async Task WithAggregateException(this Task source)
+ {
+ try
+ {
+ await source.ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ throw source.Exception ?? ex;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/FireBase.csproj b/dsa/FireBase/FireBase.csproj
new file mode 100644
index 0000000..2a47b27
--- /dev/null
+++ b/dsa/FireBase/FireBase.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp2.2</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="LiteDB" Version="4.1.4" />
+ <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
+ <PackageReference Include="System.Reactive" Version="4.1.0" />
+ </ItemGroup>
+
+</Project>
diff --git a/dsa/FireBase/FirebaseClient.cs b/dsa/FireBase/FirebaseClient.cs
new file mode 100644
index 0000000..3079f3b
--- /dev/null
+++ b/dsa/FireBase/FirebaseClient.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using Firebase.Database.Query;
+
+[assembly: InternalsVisibleTo("Firebase.Database.Tests")]
+
+namespace Firebase.Database
+{
+ /// <summary>
+ /// Firebase client which acts as an entry point to the online database.
+ /// </summary>
+ public class FirebaseClient : IDisposable
+ {
+ private readonly string baseUrl;
+ internal readonly HttpClient HttpClient;
+ internal readonly FirebaseOptions Options;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FirebaseClient" /> class.
+ /// </summary>
+ /// <param name="baseUrl"> The base url. </param>
+ /// <param name="offlineDatabaseFactory"> Offline database. </param>
+ public FirebaseClient(string baseUrl, FirebaseOptions options = null)
+ {
+ HttpClient = new HttpClient();
+ Options = options ?? new FirebaseOptions();
+
+ this.baseUrl = baseUrl;
+
+ if (!this.baseUrl.EndsWith("/")) this.baseUrl += "/";
+ }
+
+ public void Dispose()
+ {
+ HttpClient?.Dispose();
+ }
+
+ /// <summary>
+ /// Queries for a child of the data root.
+ /// </summary>
+ /// <param name="resourceName"> Name of the child. </param>
+ /// <returns> <see cref="ChildQuery" />. </returns>
+ public ChildQuery Child(string resourceName)
+ {
+ return new ChildQuery(this, () => baseUrl + resourceName);
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/FirebaseException.cs b/dsa/FireBase/FirebaseException.cs
new file mode 100644
index 0000000..cfc09d3
--- /dev/null
+++ b/dsa/FireBase/FirebaseException.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Net;
+
+namespace Firebase.Database
+{
+ public class FirebaseException : Exception
+ {
+ public FirebaseException(string requestUrl, string requestData, string responseData, HttpStatusCode statusCode)
+ : base(GenerateExceptionMessage(requestUrl, requestData, responseData))
+ {
+ RequestUrl = requestUrl;
+ RequestData = requestData;
+ ResponseData = responseData;
+ StatusCode = statusCode;
+ }
+
+ public FirebaseException(string requestUrl, string requestData, string responseData, HttpStatusCode statusCode,
+ Exception innerException)
+ : base(GenerateExceptionMessage(requestUrl, requestData, responseData), innerException)
+ {
+ RequestUrl = requestUrl;
+ RequestData = requestData;
+ ResponseData = responseData;
+ StatusCode = statusCode;
+ }
+
+ /// <summary>
+ /// Post data passed to the authentication service.
+ /// </summary>
+ public string RequestData { get; }
+
+ /// <summary>
+ /// Original url of the request.
+ /// </summary>
+ public string RequestUrl { get; }
+
+ /// <summary>
+ /// Response from the authentication service.
+ /// </summary>
+ public string ResponseData { get; }
+
+ /// <summary>
+ /// Status code of the response.
+ /// </summary>
+ public HttpStatusCode StatusCode { get; }
+
+ private static string GenerateExceptionMessage(string requestUrl, string requestData, string responseData)
+ {
+ return
+ $"Exception occured while processing the request.\nUrl: {requestUrl}\nRequest Data: {requestData}\nResponse: {responseData}";
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/FirebaseKeyGenerator.cs b/dsa/FireBase/FirebaseKeyGenerator.cs
new file mode 100644
index 0000000..37beed5
--- /dev/null
+++ b/dsa/FireBase/FirebaseKeyGenerator.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Text;
+
+namespace Firebase.Database
+{
+ /// <summary>
+ /// Offline key generator which mimics the official Firebase generators.
+ /// Credit: https://github.com/bubbafat/FirebaseSharp/blob/master/src/FirebaseSharp.Portable/FireBasePushIdGenerator.cs
+ /// </summary>
+ public class FirebaseKeyGenerator
+ {
+ // Modeled after base64 web-safe chars, but ordered by ASCII.
+ private const string PushCharsString = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
+ private static readonly char[] PushChars;
+ private static readonly DateTimeOffset Epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero);
+
+ private static readonly Random random = new Random();
+ private static readonly byte[] lastRandChars = new byte[12];
+
+ // Timestamp of last push, used to prevent local collisions if you push twice in one ms.
+ private static long lastPushTime;
+
+ static FirebaseKeyGenerator()
+ {
+ PushChars = Encoding.UTF8.GetChars(Encoding.UTF8.GetBytes(PushCharsString));
+ }
+
+ /// <summary>
+ /// Returns next firebase key based on current time.
+ /// </summary>
+ /// <returns>
+ /// The <see cref="string" />.
+ /// </returns>
+ public static string Next()
+ {
+ // We generate 72-bits of randomness which get turned into 12 characters and
+ // appended to the timestamp to prevent collisions with other clients. We store the last
+ // characters we generated because in the event of a collision, we'll use those same
+ // characters except "incremented" by one.
+ var id = new StringBuilder(20);
+ var now = (long) (DateTimeOffset.Now - Epoch).TotalMilliseconds;
+ var duplicateTime = now == lastPushTime;
+ lastPushTime = now;
+
+ var timeStampChars = new char[8];
+ for (var i = 7; i >= 0; i--)
+ {
+ var index = (int) (now % PushChars.Length);
+ timeStampChars[i] = PushChars[index];
+ now = (long) Math.Floor((double) now / PushChars.Length);
+ }
+
+ if (now != 0) throw new Exception("We should have converted the entire timestamp.");
+
+ id.Append(timeStampChars);
+
+ if (!duplicateTime)
+ {
+ for (var i = 0; i < 12; i++) lastRandChars[i] = (byte) random.Next(0, PushChars.Length);
+ }
+ else
+ {
+ // If the timestamp hasn't changed since last push, use the same random number,
+ // except incremented by 1.
+ var lastIndex = 11;
+ for (; lastIndex >= 0 && lastRandChars[lastIndex] == PushChars.Length - 1; lastIndex--)
+ lastRandChars[lastIndex] = 0;
+
+ lastRandChars[lastIndex]++;
+ }
+
+ for (var i = 0; i < 12; i++) id.Append(PushChars[lastRandChars[i]]);
+
+ if (id.Length != 20) throw new Exception("Length should be 20.");
+
+ return id.ToString();
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/FirebaseObject.cs b/dsa/FireBase/FirebaseObject.cs
new file mode 100644
index 0000000..2e0fd20
--- /dev/null
+++ b/dsa/FireBase/FirebaseObject.cs
@@ -0,0 +1,27 @@
+namespace Firebase.Database
+{
+ /// <summary>
+ /// Holds the object of type
+ /// <typeparam name="T" />
+ /// along with its key.
+ /// </summary>
+ /// <typeparam name="T"> Type of the underlying object. </typeparam>
+ public class FirebaseObject<T>
+ {
+ internal FirebaseObject(string key, T obj)
+ {
+ Key = key;
+ Object = obj;
+ }
+
+ /// <summary>
+ /// Gets the key of <see cref="Object" />.
+ /// </summary>
+ public string Key { get; }
+
+ /// <summary>
+ /// Gets the underlying object.
+ /// </summary>
+ public T Object { get; }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/FirebaseOptions.cs b/dsa/FireBase/FirebaseOptions.cs
new file mode 100644
index 0000000..b4a5e51
--- /dev/null
+++ b/dsa/FireBase/FirebaseOptions.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Firebase.Database.Offline;
+using Newtonsoft.Json;
+
+namespace Firebase.Database
+{
+ public class FirebaseOptions
+ {
+ public FirebaseOptions()
+ {
+ OfflineDatabaseFactory = (t, s) => new Dictionary<string, OfflineEntry>();
+ SubscriptionStreamReaderFactory = s => new StreamReader(s);
+ JsonSerializerSettings = new JsonSerializerSettings();
+ SyncPeriod = TimeSpan.FromSeconds(10);
+ }
+
+ /// <summary>
+ /// Gets or sets the factory for Firebase offline database. Default is in-memory dictionary.
+ /// </summary>
+ public Func<Type, string, IDictionary<string, OfflineEntry>> OfflineDatabaseFactory { get; set; }
+
+ /// <summary>
+ /// Gets or sets the method for retrieving auth tokens. Default is null.
+ /// </summary>
+ public Func<Task<string>> AuthTokenAsyncFactory { get; set; }
+
+ /// <summary>
+ /// Gets or sets the factory for <see cref="TextReader" /> used for reading online streams. Default is
+ /// <see cref="StreamReader" />.
+ /// </summary>
+ public Func<Stream, TextReader> SubscriptionStreamReaderFactory { get; set; }
+
+ /// <summary>
+ /// Gets or sets the json serializer settings.
+ /// </summary>
+ public JsonSerializerSettings JsonSerializerSettings { get; set; }
+
+ /// <summary>
+ /// Gets or sets the time between synchronization attempts for pulling and pushing offline entities. Default is 10
+ /// seconds.
+ /// </summary>
+ public TimeSpan SyncPeriod { get; set; }
+
+ /// <summary>
+ /// Specify if token returned by factory will be used as "auth" url parameter or "access_token".
+ /// </summary>
+ public bool AsAccessToken { get; set; }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Http/HttpClientExtensions.cs b/dsa/FireBase/Http/HttpClientExtensions.cs
new file mode 100644
index 0000000..6582769
--- /dev/null
+++ b/dsa/FireBase/Http/HttpClientExtensions.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace Firebase.Database.Http
+{
+ /// <summary>
+ /// The http client extensions for object deserializations.
+ /// </summary>
+ internal static class HttpClientExtensions
+ {
+ /// <summary>
+ /// The get object collection async.
+ /// </summary>
+ /// <param name="client"> The client. </param>
+ /// <param name="requestUri"> The request uri. </param>
+ /// <param name="jsonSerializerSettings"> The specific JSON Serializer Settings. </param>
+ /// <typeparam name="T"> The type of entities the collection should contain. </typeparam>
+ /// <returns> The <see cref="Task" />. </returns>
+ public static async Task<IReadOnlyCollection<FirebaseObject<T>>> GetObjectCollectionAsync<T>(
+ this HttpClient client, string requestUri,
+ JsonSerializerSettings jsonSerializerSettings)
+ {
+ var responseData = string.Empty;
+ var statusCode = HttpStatusCode.OK;
+
+ try
+ {
+ var response = await client.GetAsync(requestUri).ConfigureAwait(false);
+ statusCode = response.StatusCode;
+ responseData = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+
+ response.EnsureSuccessStatusCode();
+
+ var dictionary =
+ JsonConvert.DeserializeObject<Dictionary<string, T>>(responseData, jsonSerializerSettings);
+
+ if (dictionary == null) return new FirebaseObject<T>[0];
+
+ return dictionary.Select(item => new FirebaseObject<T>(item.Key, item.Value)).ToList();
+ }
+ catch (Exception ex)
+ {
+ throw new FirebaseException(requestUri, string.Empty, responseData, statusCode, ex);
+ }
+ }
+
+ /*/// <summary>
+ /// The get object collection async.
+ /// </summary>
+ /// <param name="client"> The client. </param>
+ /// <param name="requestUri"> The request uri. </param>
+ /// /// <param name="dataType"> The Data Type. </param>
+ /// <param name="jsonSerializerSettings"> The specific JSON Serializer Settings. </param>
+ /// <typeparam name="T"> The type of entities the collection should contain. </typeparam>
+ /// <returns> The <see cref="Task"/>. </returns>
+ public static async Task<IReadOnlyCollection<FirebaseObject<object>>> GetObjectCollectionAsync(this HttpClient client, string requestUri,
+ JsonSerializerSettings jsonSerializerSettings, Type dataType)
+ {
+ var responseData = string.Empty;
+ var statusCode = HttpStatusCode.OK;
+
+ try
+ {
+ var response = await client.GetAsync(requestUri).ConfigureAwait(false);
+ statusCode = response.StatusCode;
+ responseData = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+
+ response.EnsureSuccessStatusCode();
+
+ Type dicType = typeof(Dictionary<,>).MakeGenericType(typeof(string), dataType);
+
+ var dictionary = JsonConvert.DeserializeObject(responseData,dicType, jsonSerializerSettings) as Dictionary<string, object>;
+
+ if (dictionary == null)
+ {
+ return new FirebaseObject<object>[0];
+ }
+
+ return dictionary.Select(item => new FirebaseObject<object>(item.Key, item.Value)).ToList();
+ }
+ catch (Exception ex)
+ {
+ throw new FirebaseException(requestUri, string.Empty, responseData, statusCode, ex);
+ }
+ }*/
+
+ /// <summary>
+ /// The get object collection async.
+ /// </summary>
+ /// <param name="data"> The json data. </param>
+ /// <param name="elementType"> The type of entities the collection should contain. </param>
+ /// <returns> The <see cref="Task" />. </returns>
+ public static IEnumerable<FirebaseObject<object>> GetObjectCollection(this string data, Type elementType)
+ {
+ var dictionaryType = typeof(Dictionary<,>).MakeGenericType(typeof(string), elementType);
+ IDictionary dictionary = null;
+
+ if (data.StartsWith("["))
+ {
+ var listType = typeof(List<>).MakeGenericType(elementType);
+ var list = JsonConvert.DeserializeObject(data, listType) as IList;
+ dictionary = Activator.CreateInstance(dictionaryType) as IDictionary;
+ var index = 0;
+ foreach (var item in list) dictionary.Add(index++.ToString(), item);
+ }
+ else
+ {
+ dictionary = JsonConvert.DeserializeObject(data, dictionaryType) as IDictionary;
+ }
+
+ if (dictionary == null) yield break;
+
+ foreach (DictionaryEntry item in dictionary)
+ yield return new FirebaseObject<object>((string) item.Key, item.Value);
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Http/PostResult.cs b/dsa/FireBase/Http/PostResult.cs
new file mode 100644
index 0000000..15a4894
--- /dev/null
+++ b/dsa/FireBase/Http/PostResult.cs
@@ -0,0 +1,13 @@
+namespace Firebase.Database.Http
+{
+ /// <summary>
+ /// Represents data returned after a successful POST to firebase server.
+ /// </summary>
+ public class PostResult
+ {
+ /// <summary>
+ /// Gets or sets the generated key after a successful post.
+ /// </summary>
+ public string Name { get; set; }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/ObservableExtensions.cs b/dsa/FireBase/ObservableExtensions.cs
new file mode 100644
index 0000000..bc46261
--- /dev/null
+++ b/dsa/FireBase/ObservableExtensions.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.ObjectModel;
+using Firebase.Database.Streaming;
+
+namespace Firebase.Database
+{
+ /// <summary>
+ /// Extensions for <see cref="IObservable{T}" />.
+ /// </summary>
+ public static class ObservableExtensions
+ {
+ /// <summary>
+ /// Starts observing on given firebase observable and propagates event into an <see cref="ObservableCollection{T}" />.
+ /// </summary>
+ /// <param name="observable"> The observable. </param>
+ /// <typeparam name="T"> Type of entity. </typeparam>
+ /// <returns> The <see cref="ObservableCollection{T}" />. </returns>
+ public static ObservableCollection<T> AsObservableCollection<T>(this IObservable<FirebaseEvent<T>> observable)
+ {
+ var collection = new ObservableCollection<T>();
+
+ observable.Subscribe(f =>
+ {
+ if (f.EventType == FirebaseEventType.InsertOrUpdate)
+ {
+ var i = collection.IndexOf(f.Object);
+ if (i >= 0) collection.RemoveAt(i);
+
+ collection.Add(f.Object);
+ }
+ else
+ {
+ collection.Remove(f.Object);
+ }
+ });
+
+ return collection;
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/ConcurrentOfflineDatabase.cs b/dsa/FireBase/Offline/ConcurrentOfflineDatabase.cs
new file mode 100644
index 0000000..1a9e607
--- /dev/null
+++ b/dsa/FireBase/Offline/ConcurrentOfflineDatabase.cs
@@ -0,0 +1,233 @@
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using LiteDB;
+
+namespace Firebase.Database.Offline
+{
+ /// <summary>
+ /// The offline database.
+ /// </summary>
+ public class ConcurrentOfflineDatabase : IDictionary<string, OfflineEntry>
+ {
+ private readonly ConcurrentDictionary<string, OfflineEntry> ccache;
+ private readonly LiteRepository db;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OfflineDatabase" /> class.
+ /// </summary>
+ /// <param name="itemType"> The item type which is used to determine the database file name. </param>
+ /// <param name="filenameModifier"> Custom string which will get appended to the file name. </param>
+ public ConcurrentOfflineDatabase(Type itemType, string filenameModifier)
+ {
+ var fullName = GetFileName(itemType.ToString());
+ if (fullName.Length > 100) fullName = fullName.Substring(0, 100);
+
+ var mapper = BsonMapper.Global;
+ mapper.Entity<OfflineEntry>().Id(o => o.Key);
+
+ var root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ var filename = fullName + filenameModifier + ".db";
+ var path = Path.Combine(root, filename);
+ db = new LiteRepository(new LiteDatabase(path, mapper));
+
+ var cache = db.Database
+ .GetCollection<OfflineEntry>()
+ .FindAll()
+ .ToDictionary(o => o.Key, o => o);
+
+ ccache = new ConcurrentDictionary<string, OfflineEntry>(cache);
+ }
+
+ /// <summary>
+ /// Gets the number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1" />.
+ /// </summary>
+ /// <returns> The number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1" />. </returns>
+ public int Count => ccache.Count;
+
+ /// <summary>
+ /// Gets a value indicating whether this is a read-only collection.
+ /// </summary>
+ public bool IsReadOnly => false;
+
+ /// <summary>
+ /// Gets an <see cref="T:System.Collections.Generic.ICollection`1" /> containing the keys of the
+ /// <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="T:System.Collections.Generic.ICollection`1" /> containing the keys of the object that
+ /// implements <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </returns>
+ public ICollection<string> Keys => ccache.Keys;
+
+ /// <summary>
+ /// Gets an <see cref="T:System.Collections.Generic.ICollection`1" /> containing the values in the
+ /// <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="T:System.Collections.Generic.ICollection`1" /> containing the values in the object that
+ /// implements <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </returns>
+ public ICollection<OfflineEntry> Values => ccache.Values;
+
+ /// <summary>
+ /// Gets or sets the element with the specified key.
+ /// </summary>
+ /// <param name="key">The key of the element to get or set.</param>
+ /// <returns> The element with the specified key. </returns>
+ public OfflineEntry this[string key]
+ {
+ get => ccache[key];
+
+ set
+ {
+ ccache.AddOrUpdate(key, value, (k, existing) => value);
+ db.Upsert(value);
+ }
+ }
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the collection.
+ /// </summary>
+ /// <returns> An enumerator that can be used to iterate through the collection. </returns>
+ public IEnumerator<KeyValuePair<string, OfflineEntry>> GetEnumerator()
+ {
+ return ccache.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ /// <summary>
+ /// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1" />.
+ /// </summary>
+ /// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
+ public void Add(KeyValuePair<string, OfflineEntry> item)
+ {
+ Add(item.Key, item.Value);
+ }
+
+ /// <summary>
+ /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1" />.
+ /// </summary>
+ public void Clear()
+ {
+ ccache.Clear();
+ db.Delete<OfflineEntry>(LiteDB.Query.All());
+ }
+
+ /// <summary>
+ /// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1" /> contains a specific value.
+ /// </summary>
+ /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
+ /// <returns>
+ /// True if <paramref name="item" /> is found in the <see cref="T:System.Collections.Generic.ICollection`1" />;
+ /// otherwise, false.
+ /// </returns>
+ public bool Contains(KeyValuePair<string, OfflineEntry> item)
+ {
+ return ContainsKey(item.Key);
+ }
+
+ /// <summary>
+ /// Copies the elements of the <see cref="T:System.Collections.Generic.ICollection`1" /> to an
+ /// <see cref="T:System.Array" />, starting at a particular <see cref="T:System.Array" /> index.
+ /// </summary>
+ /// <param name="array">
+ /// The one-dimensional <see cref="T:System.Array" /> that is the destination of the elements copied
+ /// from <see cref="T:System.Collections.Generic.ICollection`1" />. The <see cref="T:System.Array" /> must have
+ /// zero-based indexing.
+ /// </param>
+ /// <param name="arrayIndex">The zero-based index in <paramref name="array" /> at which copying begins.</param>
+ public void CopyTo(KeyValuePair<string, OfflineEntry>[] array, int arrayIndex)
+ {
+ ccache.ToList().CopyTo(array, arrayIndex);
+ }
+
+ /// <summary>
+ /// Removes the first occurrence of a specific object from the
+ /// <see cref="T:System.Collections.Generic.ICollection`1" />.
+ /// </summary>
+ /// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
+ /// <returns>
+ /// True if <paramref name="item" /> was successfully removed from the
+ /// <see cref="T:System.Collections.Generic.ICollection`1" />; otherwise, false. This method also returns false if
+ /// <paramref name="item" /> is not found in the original <see cref="T:System.Collections.Generic.ICollection`1" />.
+ /// </returns>
+ public bool Remove(KeyValuePair<string, OfflineEntry> item)
+ {
+ return Remove(item.Key);
+ }
+
+ /// <summary>
+ /// Determines whether the <see cref="T:System.Collections.Generic.IDictionary`2" /> contains an element with the
+ /// specified key.
+ /// </summary>
+ /// <param name="key">The key to locate in the <see cref="T:System.Collections.Generic.IDictionary`2" />.</param>
+ /// <returns>
+ /// True if the <see cref="T:System.Collections.Generic.IDictionary`2" /> contains an element with the key;
+ /// otherwise, false.
+ /// </returns>
+ public bool ContainsKey(string key)
+ {
+ return ccache.ContainsKey(key);
+ }
+
+ /// <summary>
+ /// Adds an element with the provided key and value to the <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </summary>
+ /// <param name="key">The object to use as the key of the element to add.</param>
+ /// <param name="value">The object to use as the value of the element to add.</param>
+ public void Add(string key, OfflineEntry value)
+ {
+ ccache.AddOrUpdate(key, value, (k, existing) => value);
+ db.Upsert(value);
+ }
+
+ /// <summary>
+ /// Removes the element with the specified key from the <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </summary>
+ /// <param name="key">The key of the element to remove.</param>
+ /// <returns>
+ /// True if the element is successfully removed; otherwise, false. This method also returns false if
+ /// <paramref name="key" /> was not found in the original <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </returns>
+ public bool Remove(string key)
+ {
+ ccache.TryRemove(key, out _);
+ return db.Delete<OfflineEntry>(key);
+ }
+
+ /// <summary>
+ /// Gets the value associated with the specified key.
+ /// </summary>
+ /// <param name="key">The key whose value to get.</param>
+ /// <param name="value">
+ /// When this method returns, the value associated with the specified key, if the key is found;
+ /// otherwise, the default value for the type of the <paramref name="value" /> parameter. This parameter is passed
+ /// uninitialized.
+ /// </param>
+ /// <returns>
+ /// True if the object that implements <see cref="T:System.Collections.Generic.IDictionary`2" /> contains an
+ /// element with the specified key; otherwise, false.
+ /// </returns>
+ public bool TryGetValue(string key, out OfflineEntry value)
+ {
+ return ccache.TryGetValue(key, out value);
+ }
+
+ private string GetFileName(string fileName)
+ {
+ var invalidChars = new[] {'`', '[', ',', '='};
+ foreach (var c in invalidChars.Concat(Path.GetInvalidFileNameChars()).Distinct())
+ fileName = fileName.Replace(c, '_');
+
+ return fileName;
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/DatabaseExtensions.cs b/dsa/FireBase/Offline/DatabaseExtensions.cs
new file mode 100644
index 0000000..e7c4074
--- /dev/null
+++ b/dsa/FireBase/Offline/DatabaseExtensions.cs
@@ -0,0 +1,257 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+using Firebase.Database.Query;
+
+namespace Firebase.Database.Offline
+{
+ public static class DatabaseExtensions
+ {
+ /// <summary>
+ /// Create new instances of the <see cref="RealtimeDatabase{T}" />.
+ /// </summary>
+ /// <typeparam name="T"> Type of elements. </typeparam>
+ /// <param name="filenameModifier"> Custom string which will get appended to the file name. </param>
+ /// <param name="elementRoot"> Optional custom root element of received json items. </param>
+ /// <param name="streamingOptions"> Realtime streaming options. </param>
+ /// <param name="initialPullStrategy"> Specifies what strategy should be used for initial pulling of server data. </param>
+ /// <param name="pushChanges">
+ /// Specifies whether changed items should actually be pushed to the server. It this is false,
+ /// then Put / Post / Delete will not affect server data.
+ /// </param>
+ /// <returns> The <see cref="RealtimeDatabase{T}" />. </returns>
+ public static RealtimeDatabase<T> AsRealtimeDatabase<T>(this ChildQuery query, string filenameModifier = "",
+ string elementRoot = "", StreamingOptions streamingOptions = StreamingOptions.LatestOnly,
+ InitialPullStrategy initialPullStrategy = InitialPullStrategy.MissingOnly, bool pushChanges = true)
+ where T : class
+ {
+ return new RealtimeDatabase<T>(query, elementRoot, query.Client.Options.OfflineDatabaseFactory,
+ filenameModifier, streamingOptions, initialPullStrategy, pushChanges);
+ }
+
+ /// <summary>
+ /// Create new instances of the <see cref="RealtimeDatabase{T}" />.
+ /// </summary>
+ /// <typeparam name="T"> Type of elements. </typeparam>
+ /// <typeparam name="TSetHandler"> Type of the custom <see cref="ISetHandler{T}" /> to use. </typeparam>
+ /// <param name="filenameModifier"> Custom string which will get appended to the file name. </param>
+ /// <param name="elementRoot"> Optional custom root element of received json items. </param>
+ /// <param name="streamingOptions"> Realtime streaming options. </param>
+ /// <param name="initialPullStrategy"> Specifies what strategy should be used for initial pulling of server data. </param>
+ /// <param name="pushChanges">
+ /// Specifies whether changed items should actually be pushed to the server. It this is false,
+ /// then Put / Post / Delete will not affect server data.
+ /// </param>
+ /// <returns> The <see cref="RealtimeDatabase{T}" />. </returns>
+ public static RealtimeDatabase<T> AsRealtimeDatabase<T, TSetHandler>(this ChildQuery query,
+ string filenameModifier = "", string elementRoot = "",
+ StreamingOptions streamingOptions = StreamingOptions.LatestOnly,
+ InitialPullStrategy initialPullStrategy = InitialPullStrategy.MissingOnly, bool pushChanges = true)
+ where T : class
+ where TSetHandler : ISetHandler<T>, new()
+ {
+ return new RealtimeDatabase<T>(query, elementRoot, query.Client.Options.OfflineDatabaseFactory,
+ filenameModifier, streamingOptions, initialPullStrategy, pushChanges,
+ Activator.CreateInstance<TSetHandler>());
+ }
+
+ /// <summary>
+ /// Overwrites existing object with given key leaving any missing properties intact in firebase.
+ /// </summary>
+ /// <param name="key"> The key. </param>
+ /// <param name="obj"> The object to set. </param>
+ /// <param name="syncOnline"> Indicates whether the item should be synced online. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ public static void Patch<T>(this RealtimeDatabase<T> db, string key, T obj, bool syncOnline = true,
+ int priority = 1)
+ where T : class
+ {
+ db.Set(key, obj, syncOnline ? SyncOptions.Patch : SyncOptions.None, priority);
+ }
+
+ /// <summary>
+ /// Overwrites existing object with given key.
+ /// </summary>
+ /// <param name="key"> The key. </param>
+ /// <param name="obj"> The object to set. </param>
+ /// <param name="syncOnline"> Indicates whether the item should be synced online. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ public static void Put<T>(this RealtimeDatabase<T> db, string key, T obj, bool syncOnline = true,
+ int priority = 1)
+ where T : class
+ {
+ db.Set(key, obj, syncOnline ? SyncOptions.Put : SyncOptions.None, priority);
+ }
+
+ /// <summary>
+ /// Adds a new entity to the Database.
+ /// </summary>
+ /// <param name="obj"> The object to add. </param>
+ /// <param name="syncOnline"> Indicates whether the item should be synced online. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ /// <returns> The generated key for this object. </returns>
+ public static string Post<T>(this RealtimeDatabase<T> db, T obj, bool syncOnline = true, int priority = 1)
+ where T : class
+ {
+ var key = FirebaseKeyGenerator.Next();
+
+ db.Set(key, obj, syncOnline ? SyncOptions.Put : SyncOptions.None, priority);
+
+ return key;
+ }
+
+ /// <summary>
+ /// Deletes the entity with the given key.
+ /// </summary>
+ /// <param name="key"> The key. </param>
+ /// <param name="syncOnline"> Indicates whether the item should be synced online. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ public static void Delete<T>(this RealtimeDatabase<T> db, string key, bool syncOnline = true, int priority = 1)
+ where T : class
+ {
+ db.Set(key, null, syncOnline ? SyncOptions.Put : SyncOptions.None, priority);
+ }
+
+ /// <summary>
+ /// Do a Put for a nested property specified by <paramref name="propertyExpression" /> of an object with key
+ /// <paramref name="key" />.
+ /// </summary>
+ /// <typeparam name="T"> Type of the root elements. </typeparam>
+ /// <typeparam name="TProperty"> Type of the property being modified</typeparam>
+ /// <param name="db"> Database instance. </param>
+ /// <param name="key"> Key of the root element to modify. </param>
+ /// <param name="propertyExpression"> Expression on the root element leading to target value to modify. </param>
+ /// <param name="value"> Value to put. </param>
+ /// <param name="syncOnline"> Indicates whether the item should be synced online. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ public static void Put<T, TProperty>(this RealtimeDatabase<T> db, string key,
+ Expression<Func<T, TProperty>> propertyExpression, TProperty value, bool syncOnline = true,
+ int priority = 1)
+ where T : class
+ {
+ db.Set(key, propertyExpression, value, syncOnline ? SyncOptions.Put : SyncOptions.None, priority);
+ }
+
+ /// <summary>
+ /// Do a Patch for a nested property specified by <paramref name="propertyExpression" /> of an object with key
+ /// <paramref name="key" />.
+ /// </summary>
+ /// <typeparam name="T"> Type of the root elements. </typeparam>
+ /// <typeparam name="TProperty"> Type of the property being modified</typeparam>
+ /// <param name="db"> Database instance. </param>
+ /// <param name="key"> Key of the root element to modify. </param>
+ /// <param name="propertyExpression"> Expression on the root element leading to target value to modify. </param>
+ /// <param name="value"> Value to patch. </param>
+ /// <param name="syncOnline"> Indicates whether the item should be synced online. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ public static void Patch<T, TProperty>(this RealtimeDatabase<T> db, string key,
+ Expression<Func<T, TProperty>> propertyExpression, TProperty value, bool syncOnline = true,
+ int priority = 1)
+ where T : class
+ {
+ db.Set(key, propertyExpression, value, syncOnline ? SyncOptions.Patch : SyncOptions.None, priority);
+ }
+
+ /// <summary>
+ /// Delete a nested property specified by <paramref name="propertyExpression" /> of an object with key
+ /// <paramref name="key" />. This basically does a Put with null value.
+ /// </summary>
+ /// <typeparam name="T"> Type of the root elements. </typeparam>
+ /// <typeparam name="TProperty"> Type of the property being modified</typeparam>
+ /// <param name="db"> Database instance. </param>
+ /// <param name="key"> Key of the root element to modify. </param>
+ /// <param name="propertyExpression"> Expression on the root element leading to target value to modify. </param>
+ /// <param name="value"> Value to put. </param>
+ /// <param name="syncOnline"> Indicates whether the item should be synced online. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ public static void Delete<T, TProperty>(this RealtimeDatabase<T> db, string key,
+ Expression<Func<T, TProperty>> propertyExpression, bool syncOnline = true, int priority = 1)
+ where T : class
+ where TProperty : class
+ {
+ db.Set(key, propertyExpression, null, syncOnline ? SyncOptions.Put : SyncOptions.None, priority);
+ }
+
+ /// <summary>
+ /// Post a new entity into the nested dictionary specified by <paramref name="propertyExpression" /> of an object with
+ /// key <paramref name="key" />.
+ /// The key of the new entity is automatically generated.
+ /// </summary>
+ /// <typeparam name="T"> Type of the root elements. </typeparam>
+ /// <typeparam name="TSelector"> Type of the dictionary being modified</typeparam>
+ /// <typeparam name="TProperty"> Type of the value within the dictionary being modified</typeparam>
+ /// <param name="db"> Database instance. </param>
+ /// <param name="key"> Key of the root element to modify. </param>
+ /// <param name="propertyExpression"> Expression on the root element leading to target value to modify. </param>
+ /// <param name="value"> Value to put. </param>
+ /// <param name="syncOnline"> Indicates whether the item should be synced online. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ public static void Post<T, TSelector, TProperty>(this RealtimeDatabase<T> db, string key,
+ Expression<Func<T, TSelector>> propertyExpression, TProperty value, bool syncOnline = true,
+ int priority = 1)
+ where T : class
+ where TSelector : IDictionary<string, TProperty>
+ {
+ var nextKey = FirebaseKeyGenerator.Next();
+ var expression = Expression.Lambda<Func<T, TProperty>>(
+ Expression.Call(propertyExpression.Body,
+ typeof(TSelector).GetRuntimeMethod("get_Item", new[] {typeof(string)}),
+ Expression.Constant(nextKey)), propertyExpression.Parameters);
+ db.Set(key, expression, value, syncOnline ? SyncOptions.Put : SyncOptions.None, priority);
+ }
+
+ /// <summary>
+ /// Delete an entity with key <paramref name="dictionaryKey" /> in the nested dictionary specified by
+ /// <paramref name="propertyExpression" /> of an object with key <paramref name="key" />.
+ /// The key of the new entity is automatically generated.
+ /// </summary>
+ /// <typeparam name="T"> Type of the root elements. </typeparam>
+ /// <typeparam name="TSelector"> Type of the dictionary being modified</typeparam>
+ /// <typeparam name="TProperty"> Type of the value within the dictionary being modified</typeparam>
+ /// <param name="db"> Database instance. </param>
+ /// <param name="key"> Key of the root element to modify. </param>
+ /// <param name="propertyExpression"> Expression on the root element leading to target value to modify. </param>
+ /// <param name="dictionaryKey"> Key within the nested dictionary to delete. </param>
+ /// <param name="syncOnline"> Indicates whether the item should be synced online. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ public static void Delete<T, TProperty>(this RealtimeDatabase<T> db, string key,
+ Expression<Func<T, IDictionary<string, TProperty>>> propertyExpression, string dictionaryKey,
+ bool syncOnline = true, int priority = 1)
+ where T : class
+ {
+ var expression = Expression.Lambda<Func<T, TProperty>>(
+ Expression.Call(propertyExpression.Body,
+ typeof(IDictionary<string, TProperty>).GetRuntimeMethod("get_Item", new[] {typeof(string)}),
+ Expression.Constant(dictionaryKey)), propertyExpression.Parameters);
+ db.Set(key, expression, null, syncOnline ? SyncOptions.Put : SyncOptions.None, priority);
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/ISetHandler.cs b/dsa/FireBase/Offline/ISetHandler.cs
new file mode 100644
index 0000000..c04bd41
--- /dev/null
+++ b/dsa/FireBase/Offline/ISetHandler.cs
@@ -0,0 +1,10 @@
+using System.Threading.Tasks;
+using Firebase.Database.Query;
+
+namespace Firebase.Database.Offline
+{
+ public interface ISetHandler<in T>
+ {
+ Task SetAsync(ChildQuery query, string key, OfflineEntry entry);
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/InitialPullStrategy.cs b/dsa/FireBase/Offline/InitialPullStrategy.cs
new file mode 100644
index 0000000..ca2bebf
--- /dev/null
+++ b/dsa/FireBase/Offline/InitialPullStrategy.cs
@@ -0,0 +1,23 @@
+namespace Firebase.Database.Offline
+{
+ /// <summary>
+ /// Specifies the strategy for initial pull of server data.
+ /// </summary>
+ public enum InitialPullStrategy
+ {
+ /// <summary>
+ /// Don't pull anything.
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// Pull only what isn't already stored offline.
+ /// </summary>
+ MissingOnly,
+
+ /// <summary>
+ /// Pull everything that exists on the server.
+ /// </summary>
+ Everything
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/Internals/MemberAccessVisitor.cs b/dsa/FireBase/Offline/Internals/MemberAccessVisitor.cs
new file mode 100644
index 0000000..89a77da
--- /dev/null
+++ b/dsa/FireBase/Offline/Internals/MemberAccessVisitor.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+using Newtonsoft.Json;
+
+namespace Firebase.Database.Offline.Internals
+{
+ public class MemberAccessVisitor : ExpressionVisitor
+ {
+ private readonly IList<string> propertyNames = new List<string>();
+
+ private bool wasDictionaryAccess;
+
+ public IEnumerable<string> PropertyNames => propertyNames;
+
+ public override Expression Visit(Expression expr)
+ {
+ if (expr?.NodeType == ExpressionType.MemberAccess)
+ {
+ if (wasDictionaryAccess)
+ {
+ wasDictionaryAccess = false;
+ }
+ else
+ {
+ var memberExpr = (MemberExpression) expr;
+ var jsonAttr = memberExpr.Member.GetCustomAttribute<JsonPropertyAttribute>();
+
+ propertyNames.Add(jsonAttr?.PropertyName ?? memberExpr.Member.Name);
+ }
+ }
+ else if (expr?.NodeType == ExpressionType.Call)
+ {
+ var callExpr = (MethodCallExpression) expr;
+ if (callExpr.Method.Name == "get_Item" && callExpr.Arguments.Count == 1)
+ {
+ var e = Expression.Lambda(callExpr.Arguments[0]).Compile();
+ propertyNames.Add(e.DynamicInvoke().ToString());
+ wasDictionaryAccess = callExpr.Arguments[0].NodeType == ExpressionType.MemberAccess;
+ }
+ }
+
+ return base.Visit(expr);
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/OfflineCacheAdapter.cs b/dsa/FireBase/Offline/OfflineCacheAdapter.cs
new file mode 100644
index 0000000..3153d1b
--- /dev/null
+++ b/dsa/FireBase/Offline/OfflineCacheAdapter.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Firebase.Database.Offline
+{
+ internal class OfflineCacheAdapter<TKey, T> : IDictionary<string, T>, IDictionary
+ {
+ private readonly IDictionary<string, OfflineEntry> database;
+
+ public OfflineCacheAdapter(IDictionary<string, OfflineEntry> database)
+ {
+ this.database = database;
+ }
+
+ public void CopyTo(Array array, int index)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsSynchronized { get; }
+
+ public object SyncRoot { get; }
+
+ object IDictionary.this[object key]
+ {
+ get => database[key.ToString()].Deserialize<T>();
+
+ set
+ {
+ var keyString = key.ToString();
+ if (database.ContainsKey(keyString))
+ database[keyString] = new OfflineEntry(keyString, value, database[keyString].Priority,
+ database[keyString].SyncOptions);
+ else
+ database[keyString] = new OfflineEntry(keyString, value, 1, SyncOptions.None);
+ }
+ }
+
+ ICollection IDictionary.Values { get; }
+
+ ICollection IDictionary.Keys { get; }
+
+ public bool Contains(object key)
+ {
+ return ContainsKey(key.ToString());
+ }
+
+ IDictionaryEnumerator IDictionary.GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Remove(object key)
+ {
+ Remove(key.ToString());
+ }
+
+ public bool IsFixedSize => false;
+
+ public void Add(object key, object value)
+ {
+ Add(key.ToString(), (T) value);
+ }
+
+ public int Count => database.Count;
+
+ public bool IsReadOnly => database.IsReadOnly;
+
+ public ICollection<string> Keys => database.Keys;
+
+ public ICollection<T> Values => database.Values.Select(o => o.Deserialize<T>()).ToList();
+
+ public T this[string key]
+ {
+ get => database[key].Deserialize<T>();
+
+ set
+ {
+ if (database.ContainsKey(key))
+ database[key] = new OfflineEntry(key, value, database[key].Priority, database[key].SyncOptions);
+ else
+ database[key] = new OfflineEntry(key, value, 1, SyncOptions.None);
+ }
+ }
+
+ public IEnumerator<KeyValuePair<string, T>> GetEnumerator()
+ {
+ return database.Select(d => new KeyValuePair<string, T>(d.Key, d.Value.Deserialize<T>())).GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public void Add(KeyValuePair<string, T> item)
+ {
+ Add(item.Key, item.Value);
+ }
+
+ public void Clear()
+ {
+ database.Clear();
+ }
+
+ public bool Contains(KeyValuePair<string, T> item)
+ {
+ return ContainsKey(item.Key);
+ }
+
+ public void CopyTo(KeyValuePair<string, T>[] array, int arrayIndex)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool Remove(KeyValuePair<string, T> item)
+ {
+ return database.Remove(item.Key);
+ }
+
+ public void Add(string key, T value)
+ {
+ database.Add(key, new OfflineEntry(key, value, 1, SyncOptions.None));
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return database.ContainsKey(key);
+ }
+
+ public bool Remove(string key)
+ {
+ return database.Remove(key);
+ }
+
+ public bool TryGetValue(string key, out T value)
+ {
+ OfflineEntry val;
+
+ if (database.TryGetValue(key, out val))
+ {
+ value = val.Deserialize<T>();
+ return true;
+ }
+
+ value = default(T);
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/OfflineDatabase.cs b/dsa/FireBase/Offline/OfflineDatabase.cs
new file mode 100644
index 0000000..be0380b
--- /dev/null
+++ b/dsa/FireBase/Offline/OfflineDatabase.cs
@@ -0,0 +1,228 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using LiteDB;
+
+namespace Firebase.Database.Offline
+{
+ /// <summary>
+ /// The offline database.
+ /// </summary>
+ public class OfflineDatabase : IDictionary<string, OfflineEntry>
+ {
+ private readonly IDictionary<string, OfflineEntry> cache;
+ private readonly LiteRepository db;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OfflineDatabase" /> class.
+ /// </summary>
+ /// <param name="itemType"> The item type which is used to determine the database file name. </param>
+ /// <param name="filenameModifier"> Custom string which will get appended to the file name. </param>
+ public OfflineDatabase(Type itemType, string filenameModifier)
+ {
+ var fullName = GetFileName(itemType.ToString());
+ if (fullName.Length > 100) fullName = fullName.Substring(0, 100);
+
+ var mapper = BsonMapper.Global;
+ mapper.Entity<OfflineEntry>().Id(o => o.Key);
+
+ var root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ var filename = fullName + filenameModifier + ".db";
+ var path = Path.Combine(root, filename);
+ db = new LiteRepository(new LiteDatabase(path, mapper));
+
+ cache = db.Database.GetCollection<OfflineEntry>().FindAll()
+ .ToDictionary(o => o.Key, o => o);
+ }
+
+ /// <summary>
+ /// Gets the number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1" />.
+ /// </summary>
+ /// <returns> The number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1" />. </returns>
+ public int Count => cache.Count;
+
+ /// <summary>
+ /// Gets a value indicating whether this is a read-only collection.
+ /// </summary>
+ public bool IsReadOnly => cache.IsReadOnly;
+
+ /// <summary>
+ /// Gets an <see cref="T:System.Collections.Generic.ICollection`1" /> containing the keys of the
+ /// <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="T:System.Collections.Generic.ICollection`1" /> containing the keys of the object that
+ /// implements <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </returns>
+ public ICollection<string> Keys => cache.Keys;
+
+ /// <summary>
+ /// Gets an <see cref="T:System.Collections.Generic.ICollection`1" /> containing the values in the
+ /// <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="T:System.Collections.Generic.ICollection`1" /> containing the values in the object that
+ /// implements <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </returns>
+ public ICollection<OfflineEntry> Values => cache.Values;
+
+ /// <summary>
+ /// Gets or sets the element with the specified key.
+ /// </summary>
+ /// <param name="key">The key of the element to get or set.</param>
+ /// <returns> The element with the specified key. </returns>
+ public OfflineEntry this[string key]
+ {
+ get => cache[key];
+
+ set
+ {
+ cache[key] = value;
+ db.Upsert(value);
+ }
+ }
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the collection.
+ /// </summary>
+ /// <returns> An enumerator that can be used to iterate through the collection. </returns>
+ public IEnumerator<KeyValuePair<string, OfflineEntry>> GetEnumerator()
+ {
+ return cache.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ /// <summary>
+ /// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1" />.
+ /// </summary>
+ /// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
+ public void Add(KeyValuePair<string, OfflineEntry> item)
+ {
+ Add(item.Key, item.Value);
+ }
+
+ /// <summary>
+ /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1" />.
+ /// </summary>
+ public void Clear()
+ {
+ cache.Clear();
+ db.Delete<OfflineEntry>(LiteDB.Query.All());
+ }
+
+ /// <summary>
+ /// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1" /> contains a specific value.
+ /// </summary>
+ /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
+ /// <returns>
+ /// True if <paramref name="item" /> is found in the <see cref="T:System.Collections.Generic.ICollection`1" />;
+ /// otherwise, false.
+ /// </returns>
+ public bool Contains(KeyValuePair<string, OfflineEntry> item)
+ {
+ return ContainsKey(item.Key);
+ }
+
+ /// <summary>
+ /// Copies the elements of the <see cref="T:System.Collections.Generic.ICollection`1" /> to an
+ /// <see cref="T:System.Array" />, starting at a particular <see cref="T:System.Array" /> index.
+ /// </summary>
+ /// <param name="array">
+ /// The one-dimensional <see cref="T:System.Array" /> that is the destination of the elements copied
+ /// from <see cref="T:System.Collections.Generic.ICollection`1" />. The <see cref="T:System.Array" /> must have
+ /// zero-based indexing.
+ /// </param>
+ /// <param name="arrayIndex">The zero-based index in <paramref name="array" /> at which copying begins.</param>
+ public void CopyTo(KeyValuePair<string, OfflineEntry>[] array, int arrayIndex)
+ {
+ cache.CopyTo(array, arrayIndex);
+ }
+
+ /// <summary>
+ /// Removes the first occurrence of a specific object from the
+ /// <see cref="T:System.Collections.Generic.ICollection`1" />.
+ /// </summary>
+ /// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
+ /// <returns>
+ /// True if <paramref name="item" /> was successfully removed from the
+ /// <see cref="T:System.Collections.Generic.ICollection`1" />; otherwise, false. This method also returns false if
+ /// <paramref name="item" /> is not found in the original <see cref="T:System.Collections.Generic.ICollection`1" />.
+ /// </returns>
+ public bool Remove(KeyValuePair<string, OfflineEntry> item)
+ {
+ return Remove(item.Key);
+ }
+
+ /// <summary>
+ /// Determines whether the <see cref="T:System.Collections.Generic.IDictionary`2" /> contains an element with the
+ /// specified key.
+ /// </summary>
+ /// <param name="key">The key to locate in the <see cref="T:System.Collections.Generic.IDictionary`2" />.</param>
+ /// <returns>
+ /// True if the <see cref="T:System.Collections.Generic.IDictionary`2" /> contains an element with the key;
+ /// otherwise, false.
+ /// </returns>
+ public bool ContainsKey(string key)
+ {
+ return cache.ContainsKey(key);
+ }
+
+ /// <summary>
+ /// Adds an element with the provided key and value to the <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </summary>
+ /// <param name="key">The object to use as the key of the element to add.</param>
+ /// <param name="value">The object to use as the value of the element to add.</param>
+ public void Add(string key, OfflineEntry value)
+ {
+ cache.Add(key, value);
+ db.Insert(value);
+ }
+
+ /// <summary>
+ /// Removes the element with the specified key from the <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </summary>
+ /// <param name="key">The key of the element to remove.</param>
+ /// <returns>
+ /// True if the element is successfully removed; otherwise, false. This method also returns false if
+ /// <paramref name="key" /> was not found in the original <see cref="T:System.Collections.Generic.IDictionary`2" />.
+ /// </returns>
+ public bool Remove(string key)
+ {
+ cache.Remove(key);
+ return db.Delete<OfflineEntry>(key);
+ }
+
+ /// <summary>
+ /// Gets the value associated with the specified key.
+ /// </summary>
+ /// <param name="key">The key whose value to get.</param>
+ /// <param name="value">
+ /// When this method returns, the value associated with the specified key, if the key is found;
+ /// otherwise, the default value for the type of the <paramref name="value" /> parameter. This parameter is passed
+ /// uninitialized.
+ /// </param>
+ /// <returns>
+ /// True if the object that implements <see cref="T:System.Collections.Generic.IDictionary`2" /> contains an
+ /// element with the specified key; otherwise, false.
+ /// </returns>
+ public bool TryGetValue(string key, out OfflineEntry value)
+ {
+ return cache.TryGetValue(key, out value);
+ }
+
+ private string GetFileName(string fileName)
+ {
+ var invalidChars = new[] {'`', '[', ',', '='};
+ foreach (var c in invalidChars.Concat(Path.GetInvalidFileNameChars()).Distinct())
+ fileName = fileName.Replace(c, '_');
+
+ return fileName;
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/OfflineEntry.cs b/dsa/FireBase/Offline/OfflineEntry.cs
new file mode 100644
index 0000000..9feffa3
--- /dev/null
+++ b/dsa/FireBase/Offline/OfflineEntry.cs
@@ -0,0 +1,99 @@
+using System;
+using Newtonsoft.Json;
+
+namespace Firebase.Database.Offline
+{
+ /// <summary>
+ /// Represents an object stored in offline storage.
+ /// </summary>
+ public class OfflineEntry
+ {
+ private object dataInstance;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OfflineEntry" /> class with an already serialized object.
+ /// </summary>
+ /// <param name="key"> The key. </param>
+ /// <param name="obj"> The object. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ /// <param name="syncOptions"> The sync options. </param>
+ public OfflineEntry(string key, object obj, string data, int priority, SyncOptions syncOptions,
+ bool isPartial = false)
+ {
+ Key = key;
+ Priority = priority;
+ Data = data;
+ Timestamp = DateTime.UtcNow;
+ SyncOptions = syncOptions;
+ IsPartial = isPartial;
+
+ dataInstance = obj;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OfflineEntry" /> class.
+ /// </summary>
+ /// <param name="key"> The key. </param>
+ /// <param name="obj"> The object. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ /// <param name="syncOptions"> The sync options. </param>
+ public OfflineEntry(string key, object obj, int priority, SyncOptions syncOptions, bool isPartial = false)
+ : this(key, obj, JsonConvert.SerializeObject(obj), priority, syncOptions, isPartial)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OfflineEntry" /> class.
+ /// </summary>
+ public OfflineEntry()
+ {
+ }
+
+ /// <summary>
+ /// Gets or sets the key of this entry.
+ /// </summary>
+ public string Key { get; set; }
+
+ /// <summary>
+ /// Gets or sets the priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </summary>
+ public int Priority { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timestamp when this entry was last touched.
+ /// </summary>
+ public DateTime Timestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="SyncOptions" /> which define what sync state this entry is in.
+ /// </summary>
+ public SyncOptions SyncOptions { get; set; }
+
+ /// <summary>
+ /// Gets or sets serialized JSON data.
+ /// </summary>
+ public string Data { get; set; }
+
+ /// <summary>
+ /// Specifies whether this is only a partial object.
+ /// </summary>
+ public bool IsPartial { get; set; }
+
+ /// <summary>
+ /// Deserializes <see cref="Data" /> into <typeparamref name="T" />. The result is cached.
+ /// </summary>
+ /// <typeparam name="T"> Type of object to deserialize into. </typeparam>
+ /// <returns> Instance of <typeparamref name="T" />. </returns>
+ public T Deserialize<T>()
+ {
+ return (T) (dataInstance ?? (dataInstance = JsonConvert.DeserializeObject<T>(Data)));
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/RealtimeDatabase.cs b/dsa/FireBase/Offline/RealtimeDatabase.cs
new file mode 100644
index 0000000..973db46
--- /dev/null
+++ b/dsa/FireBase/Offline/RealtimeDatabase.cs
@@ -0,0 +1,479 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Net;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Reactive.Threading.Tasks;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Firebase.Database.Extensions;
+using Firebase.Database.Offline.Internals;
+using Firebase.Database.Query;
+using Firebase.Database.Streaming;
+using Newtonsoft.Json;
+
+namespace Firebase.Database.Offline
+{
+ /// <summary>
+ /// The real-time Database which synchronizes online and offline data.
+ /// </summary>
+ /// <typeparam name="T"> Type of entities. </typeparam>
+ public class RealtimeDatabase<T> : IDisposable where T : class
+ {
+ private readonly ChildQuery childQuery;
+ private readonly string elementRoot;
+ private readonly FirebaseCache<T> firebaseCache;
+ private readonly InitialPullStrategy initialPullStrategy;
+ private readonly bool pushChanges;
+ private readonly StreamingOptions streamingOptions;
+ private readonly Subject<FirebaseEvent<T>> subject;
+ private FirebaseSubscription<T> firebaseSubscription;
+
+ private bool isSyncRunning;
+ private IObservable<FirebaseEvent<T>> observable;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RealtimeDatabase{T}" /> class.
+ /// </summary>
+ /// <param name="childQuery"> The child query. </param>
+ /// <param name="elementRoot"> The element Root. </param>
+ /// <param name="offlineDatabaseFactory"> The offline database factory. </param>
+ /// <param name="filenameModifier"> Custom string which will get appended to the file name. </param>
+ /// <param name="streamChanges"> Specifies whether changes should be streamed from the server. </param>
+ /// <param name="pullEverythingOnStart">
+ /// Specifies if everything should be pull from the online storage on start. It only
+ /// makes sense when <see cref="streamChanges" /> is set to true.
+ /// </param>
+ /// <param name="pushChanges">
+ /// Specifies whether changed items should actually be pushed to the server. If this is false,
+ /// then Put / Post / Delete will not affect server data.
+ /// </param>
+ public RealtimeDatabase(ChildQuery childQuery, string elementRoot,
+ Func<Type, string, IDictionary<string, OfflineEntry>> offlineDatabaseFactory, string filenameModifier,
+ StreamingOptions streamingOptions, InitialPullStrategy initialPullStrategy, bool pushChanges,
+ ISetHandler<T> setHandler = null)
+ {
+ this.childQuery = childQuery;
+ this.elementRoot = elementRoot;
+ this.streamingOptions = streamingOptions;
+ this.initialPullStrategy = initialPullStrategy;
+ this.pushChanges = pushChanges;
+ Database = offlineDatabaseFactory(typeof(T), filenameModifier);
+ firebaseCache = new FirebaseCache<T>(new OfflineCacheAdapter<string, T>(Database));
+ subject = new Subject<FirebaseEvent<T>>();
+
+ PutHandler = setHandler ?? new SetHandler<T>();
+
+ isSyncRunning = true;
+ Task.Factory.StartNew(SynchronizeThread, CancellationToken.None, TaskCreationOptions.LongRunning,
+ TaskScheduler.Default);
+ }
+
+ /// <summary>
+ /// Gets the backing Database.
+ /// </summary>
+ public IDictionary<string, OfflineEntry> Database { get; }
+
+ public ISetHandler<T> PutHandler { private get; set; }
+
+ public void Dispose()
+ {
+ subject.OnCompleted();
+ firebaseSubscription?.Dispose();
+ }
+
+ /// <summary>
+ /// Event raised whenever an exception is thrown in the synchronization thread. Exception thrown in there are
+ /// swallowed, so this event is the only way to get to them.
+ /// </summary>
+ public event EventHandler<ExceptionEventArgs> SyncExceptionThrown;
+
+ /// <summary>
+ /// Overwrites existing object with given key.
+ /// </summary>
+ /// <param name="key"> The key. </param>
+ /// <param name="obj"> The object to set. </param>
+ /// <param name="syncOnline"> Indicates whether the item should be synced online. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ public void Set(string key, T obj, SyncOptions syncOptions, int priority = 1)
+ {
+ SetAndRaise(key, new OfflineEntry(key, obj, priority, syncOptions));
+ }
+
+ public void Set<TProperty>(string key, Expression<Func<T, TProperty>> propertyExpression, object value,
+ SyncOptions syncOptions, int priority = 1)
+ {
+ var fullKey = GenerateFullKey(key, propertyExpression, syncOptions);
+ var serializedObject = JsonConvert.SerializeObject(value).Trim('"', '\\');
+
+ if (fullKey.Item3)
+ {
+ if (typeof(TProperty) != typeof(string) || value == null)
+ // don't escape non-string primitives and null;
+ serializedObject = $"{{ \"{fullKey.Item2}\" : {serializedObject} }}";
+ else
+ serializedObject = $"{{ \"{fullKey.Item2}\" : \"{serializedObject}\" }}";
+ }
+
+ var setObject = firebaseCache.PushData("/" + fullKey.Item1, serializedObject).First();
+
+ if (!Database.ContainsKey(key) || Database[key].SyncOptions != SyncOptions.Patch &&
+ Database[key].SyncOptions != SyncOptions.Put)
+ Database[fullKey.Item1] =
+ new OfflineEntry(fullKey.Item1, value, serializedObject, priority, syncOptions, true);
+
+ subject.OnNext(new FirebaseEvent<T>(key, setObject.Object,
+ setObject == null ? FirebaseEventType.Delete : FirebaseEventType.InsertOrUpdate,
+ FirebaseEventSource.Offline));
+ }
+
+ /// <summary>
+ /// Fetches an object with the given key and adds it to the Database.
+ /// </summary>
+ /// <param name="key"> The key. </param>
+ /// <param name="priority">
+ /// The priority. Objects with higher priority will be synced first. Higher number indicates higher
+ /// priority.
+ /// </param>
+ public void Pull(string key, int priority = 1)
+ {
+ if (!Database.ContainsKey(key))
+ Database[key] = new OfflineEntry(key, null, priority, SyncOptions.Pull);
+ else if (Database[key].SyncOptions == SyncOptions.None)
+ // pull only if push isn't pending
+ Database[key].SyncOptions = SyncOptions.Pull;
+ }
+
+ /// <summary>
+ /// Fetches everything from the remote database.
+ /// </summary>
+ public async Task PullAsync()
+ {
+ var existingEntries = await childQuery
+ .OnceAsync<T>()
+ .ToObservable()
+ .RetryAfterDelay<IReadOnlyCollection<FirebaseObject<T>>, FirebaseException>(
+ childQuery.Client.Options.SyncPeriod,
+ ex => ex.StatusCode ==
+ HttpStatusCode
+ .OK) // OK implies the request couldn't complete due to network error.
+ .Select(e => ResetDatabaseFromInitial(e, false))
+ .SelectMany(e => e)
+ .Do(e =>
+ {
+ Database[e.Key] = new OfflineEntry(e.Key, e.Object, 1, SyncOptions.None);
+ subject.OnNext(new FirebaseEvent<T>(e.Key, e.Object, FirebaseEventType.InsertOrUpdate,
+ FirebaseEventSource.OnlinePull));
+ })
+ .ToList();
+
+ // Remove items not stored online
+ foreach (var item in Database.Keys.Except(existingEntries.Select(f => f.Key)).ToList())
+ {
+ Database.Remove(item);
+ subject.OnNext(new FirebaseEvent<T>(item, null, FirebaseEventType.Delete,
+ FirebaseEventSource.OnlinePull));
+ }
+ }
+
+ /// <summary>
+ /// Retrieves all offline items currently stored in local database.
+ /// </summary>
+ public IEnumerable<FirebaseObject<T>> Once()
+ {
+ return Database
+ .Where(kvp => !string.IsNullOrEmpty(kvp.Value.Data) && kvp.Value.Data != "null" && !kvp.Value.IsPartial)
+ .Select(kvp => new FirebaseObject<T>(kvp.Key, kvp.Value.Deserialize<T>()))
+ .ToList();
+ }
+
+ /// <summary>
+ /// Starts observing the real-time Database. Events will be fired both when change is done locally and remotely.
+ /// </summary>
+ /// <returns> Stream of <see cref="FirebaseEvent{T}" />. </returns>
+ public IObservable<FirebaseEvent<T>> AsObservable()
+ {
+ if (!isSyncRunning)
+ {
+ isSyncRunning = true;
+ Task.Factory.StartNew(SynchronizeThread, CancellationToken.None, TaskCreationOptions.LongRunning,
+ TaskScheduler.Default);
+ }
+
+ if (observable == null)
+ {
+ var initialData = Observable.Return(FirebaseEvent<T>.Empty(FirebaseEventSource.Offline));
+ if (Database.TryGetValue(elementRoot, out var oe))
+ initialData = Observable.Return(oe)
+ .Where(offlineEntry =>
+ !string.IsNullOrEmpty(offlineEntry.Data) && offlineEntry.Data != "null" &&
+ !offlineEntry.IsPartial)
+ .Select(offlineEntry => new FirebaseEvent<T>(offlineEntry.Key, offlineEntry.Deserialize<T>(),
+ FirebaseEventType.InsertOrUpdate, FirebaseEventSource.Offline));
+ else if (Database.Count > 0)
+ initialData = Database
+ .Where(kvp =>
+ !string.IsNullOrEmpty(kvp.Value.Data) && kvp.Value.Data != "null" && !kvp.Value.IsPartial)
+ .Select(kvp => new FirebaseEvent<T>(kvp.Key, kvp.Value.Deserialize<T>(),
+ FirebaseEventType.InsertOrUpdate, FirebaseEventSource.Offline))
+ .ToList()
+ .ToObservable();
+
+ observable = initialData
+ .Merge(subject)
+ .Merge(GetInitialPullObservable()
+ .RetryAfterDelay<IReadOnlyCollection<FirebaseObject<T>>, FirebaseException>(
+ childQuery.Client.Options.SyncPeriod,
+ ex => ex.StatusCode ==
+ HttpStatusCode
+ .OK) // OK implies the request couldn't complete due to network error.
+ .Select(e => ResetDatabaseFromInitial(e))
+ .SelectMany(e => e)
+ .Do(SetObjectFromInitialPull)
+ .Select(e => new FirebaseEvent<T>(e.Key, e.Object,
+ e.Object == null ? FirebaseEventType.Delete : FirebaseEventType.InsertOrUpdate,
+ FirebaseEventSource.OnlineInitial))
+ .Concat(Observable.Create<FirebaseEvent<T>>(observer =>
+ InitializeStreamingSubscription(observer))))
+ .Do(next => { }, e => observable = null, () => observable = null)
+ .Replay()
+ .RefCount();
+ }
+
+ return observable;
+ }
+
+ private IReadOnlyCollection<FirebaseObject<T>> ResetDatabaseFromInitial(
+ IReadOnlyCollection<FirebaseObject<T>> collection, bool onlyWhenInitialEverything = true)
+ {
+ if (onlyWhenInitialEverything && initialPullStrategy != InitialPullStrategy.Everything) return collection;
+
+ // items which are in local db, but not in the online collection
+ var extra = Once()
+ .Select(f => f.Key)
+ .Except(collection.Select(c => c.Key))
+ .Select(k => new FirebaseObject<T>(k, null));
+
+ return collection.Concat(extra).ToList();
+ }
+
+ private void SetObjectFromInitialPull(FirebaseObject<T> e)
+ {
+ // set object with no sync only if it doesn't exist yet
+ // and the InitialPullStrategy != Everything
+ // this attempts to deal with scenario when you are offline, have local changes and go online
+ // in this case having the InitialPullStrategy set to everything would basically purge all local changes
+ if (!Database.ContainsKey(e.Key) || Database[e.Key].SyncOptions == SyncOptions.None ||
+ Database[e.Key].SyncOptions == SyncOptions.Pull ||
+ initialPullStrategy != InitialPullStrategy.Everything)
+ Database[e.Key] = new OfflineEntry(e.Key, e.Object, 1, SyncOptions.None);
+ }
+
+ private IObservable<IReadOnlyCollection<FirebaseObject<T>>> GetInitialPullObservable()
+ {
+ FirebaseQuery query;
+ switch (initialPullStrategy)
+ {
+ case InitialPullStrategy.MissingOnly:
+ query = childQuery.OrderByKey().StartAt(() => GetLatestKey());
+ break;
+ case InitialPullStrategy.Everything:
+ query = childQuery;
+ break;
+ case InitialPullStrategy.None:
+ default:
+ return Observable.Empty<IReadOnlyCollection<FirebaseEvent<T>>>();
+ }
+
+ if (string.IsNullOrWhiteSpace(elementRoot))
+ return Observable.Defer(() => query.OnceAsync<T>().ToObservable());
+
+ // there is an element root, which indicates the target location is not a collection but a single element
+ return Observable.Defer(async () =>
+ Observable.Return(await query.OnceSingleAsync<T>())
+ .Select(e => new[] {new FirebaseObject<T>(elementRoot, e)}));
+ }
+
+ private IDisposable InitializeStreamingSubscription(IObserver<FirebaseEvent<T>> observer)
+ {
+ var completeDisposable = Disposable.Create(() => isSyncRunning = false);
+
+ switch (streamingOptions)
+ {
+ case StreamingOptions.LatestOnly:
+ // stream since the latest key
+ var queryLatest = childQuery.OrderByKey().StartAt(() => GetLatestKey());
+ firebaseSubscription =
+ new FirebaseSubscription<T>(observer, queryLatest, elementRoot, firebaseCache);
+ firebaseSubscription.ExceptionThrown += StreamingExceptionThrown;
+
+ return new CompositeDisposable(firebaseSubscription.Run(), completeDisposable);
+ case StreamingOptions.Everything:
+ // stream everything
+ var queryAll = childQuery;
+ firebaseSubscription = new FirebaseSubscription<T>(observer, queryAll, elementRoot, firebaseCache);
+ firebaseSubscription.ExceptionThrown += StreamingExceptionThrown;
+
+ return new CompositeDisposable(firebaseSubscription.Run(), completeDisposable);
+ }
+
+ return completeDisposable;
+ }
+
+ private void SetAndRaise(string key, OfflineEntry obj,
+ FirebaseEventSource eventSource = FirebaseEventSource.Offline)
+ {
+ Database[key] = obj;
+ subject.OnNext(new FirebaseEvent<T>(key, obj?.Deserialize<T>(),
+ string.IsNullOrEmpty(obj?.Data) || obj?.Data == "null"
+ ? FirebaseEventType.Delete
+ : FirebaseEventType.InsertOrUpdate, eventSource));
+ }
+
+ private async void SynchronizeThread()
+ {
+ while (isSyncRunning)
+ {
+ try
+ {
+ var validEntries = Database.Where(e => e.Value != null);
+ await PullEntriesAsync(validEntries.Where(kvp => kvp.Value.SyncOptions == SyncOptions.Pull));
+
+ if (pushChanges)
+ await PushEntriesAsync(validEntries.Where(kvp =>
+ kvp.Value.SyncOptions == SyncOptions.Put || kvp.Value.SyncOptions == SyncOptions.Patch));
+ }
+ catch (Exception ex)
+ {
+ SyncExceptionThrown?.Invoke(this, new ExceptionEventArgs(ex));
+ }
+
+ await Task.Delay(childQuery.Client.Options.SyncPeriod);
+ }
+ }
+
+ private string GetLatestKey()
+ {
+ var key = Database.OrderBy(o => o.Key, StringComparer.Ordinal).LastOrDefault().Key ?? string.Empty;
+
+ if (!string.IsNullOrWhiteSpace(key))
+ key = key.Substring(0, key.Length - 1) + (char) (key[key.Length - 1] + 1);
+
+ return key;
+ }
+
+ private async Task PushEntriesAsync(IEnumerable<KeyValuePair<string, OfflineEntry>> pushEntries)
+ {
+ var groups = pushEntries.GroupBy(pair => pair.Value.Priority).OrderByDescending(kvp => kvp.Key).ToList();
+
+ foreach (var group in groups)
+ {
+ var tasks = group.OrderBy(kvp => kvp.Value.IsPartial).Select(kvp =>
+ kvp.Value.IsPartial
+ ? ResetSyncAfterPush(PutHandler.SetAsync(childQuery, kvp.Key, kvp.Value), kvp.Key)
+ : ResetSyncAfterPush(PutHandler.SetAsync(childQuery, kvp.Key, kvp.Value), kvp.Key,
+ kvp.Value.Deserialize<T>()));
+
+ try
+ {
+ await Task.WhenAll(tasks).WithAggregateException();
+ }
+ catch (Exception ex)
+ {
+ SyncExceptionThrown?.Invoke(this, new ExceptionEventArgs(ex));
+ }
+ }
+ }
+
+ private async Task PullEntriesAsync(IEnumerable<KeyValuePair<string, OfflineEntry>> pullEntries)
+ {
+ var taskGroups = pullEntries.GroupBy(pair => pair.Value.Priority).OrderByDescending(kvp => kvp.Key);
+
+ foreach (var group in taskGroups)
+ {
+ var tasks = group.Select(pair =>
+ ResetAfterPull(
+ childQuery.Child(pair.Key == elementRoot ? string.Empty : pair.Key).OnceSingleAsync<T>(),
+ pair.Key, pair.Value));
+
+ try
+ {
+ await Task.WhenAll(tasks).WithAggregateException();
+ }
+ catch (Exception ex)
+ {
+ SyncExceptionThrown?.Invoke(this, new ExceptionEventArgs(ex));
+ }
+ }
+ }
+
+ private async Task ResetAfterPull(Task<T> task, string key, OfflineEntry entry)
+ {
+ await task;
+ SetAndRaise(key, new OfflineEntry(key, task.Result, entry.Priority, SyncOptions.None),
+ FirebaseEventSource.OnlinePull);
+ }
+
+ private async Task ResetSyncAfterPush(Task task, string key, T obj)
+ {
+ await ResetSyncAfterPush(task, key);
+
+ if (streamingOptions == StreamingOptions.None)
+ subject.OnNext(new FirebaseEvent<T>(key, obj,
+ obj == null ? FirebaseEventType.Delete : FirebaseEventType.InsertOrUpdate,
+ FirebaseEventSource.OnlinePush));
+ }
+
+ private async Task ResetSyncAfterPush(Task task, string key)
+ {
+ await task;
+ ResetSyncOptions(key);
+ }
+
+ private void ResetSyncOptions(string key)
+ {
+ var item = Database[key];
+
+ if (item.IsPartial)
+ {
+ Database.Remove(key);
+ }
+ else
+ {
+ item.SyncOptions = SyncOptions.None;
+ Database[key] = item;
+ }
+ }
+
+ private void StreamingExceptionThrown(object sender, ExceptionEventArgs<FirebaseException> e)
+ {
+ SyncExceptionThrown?.Invoke(this, new ExceptionEventArgs(e.Exception));
+ }
+
+ private Tuple<string, string, bool> GenerateFullKey<TProperty>(string key,
+ Expression<Func<T, TProperty>> propertyGetter, SyncOptions syncOptions)
+ {
+ var visitor = new MemberAccessVisitor();
+ visitor.Visit(propertyGetter);
+ var propertyType = typeof(TProperty).GetTypeInfo();
+ var prefix = key == string.Empty ? string.Empty : key + "/";
+
+ // primitive types
+ if (syncOptions == SyncOptions.Patch && (propertyType.IsPrimitive ||
+ Nullable.GetUnderlyingType(typeof(TProperty)) != null ||
+ typeof(TProperty) == typeof(string)))
+ return Tuple.Create(prefix + string.Join("/", visitor.PropertyNames.Skip(1).Reverse()),
+ visitor.PropertyNames.First(), true);
+
+ return Tuple.Create(prefix + string.Join("/", visitor.PropertyNames.Reverse()),
+ visitor.PropertyNames.First(), false);
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/SetHandler.cs b/dsa/FireBase/Offline/SetHandler.cs
new file mode 100644
index 0000000..6314c3c
--- /dev/null
+++ b/dsa/FireBase/Offline/SetHandler.cs
@@ -0,0 +1,19 @@
+using System.Threading.Tasks;
+using Firebase.Database.Query;
+
+namespace Firebase.Database.Offline
+{
+ public class SetHandler<T> : ISetHandler<T>
+ {
+ public virtual async Task SetAsync(ChildQuery query, string key, OfflineEntry entry)
+ {
+ using (var child = query.Child(key))
+ {
+ if (entry.SyncOptions == SyncOptions.Put)
+ await child.PutAsync(entry.Data);
+ else
+ await child.PatchAsync(entry.Data);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/StreamingOptions.cs b/dsa/FireBase/Offline/StreamingOptions.cs
new file mode 100644
index 0000000..a420cbb
--- /dev/null
+++ b/dsa/FireBase/Offline/StreamingOptions.cs
@@ -0,0 +1,23 @@
+namespace Firebase.Database.Offline
+{
+ public enum StreamingOptions
+ {
+ /// <summary>
+ /// No realtime streaming.
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// Streaming of only new items - not the existing ones.
+ /// </summary>
+ LatestOnly,
+
+ /// <summary>
+ /// Streaming of all items. This will also pull all existing items on start, so be mindful about the number of items in
+ /// your DB.
+ /// When used, consider not setting the <see cref="InitialPullStrategy" /> to
+ /// <see cref="InitialPullStrategy.Everything" /> because you would pointlessly pull everything twice.
+ /// </summary>
+ Everything
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Offline/SyncOptions.cs b/dsa/FireBase/Offline/SyncOptions.cs
new file mode 100644
index 0000000..ca68d0a
--- /dev/null
+++ b/dsa/FireBase/Offline/SyncOptions.cs
@@ -0,0 +1,28 @@
+namespace Firebase.Database.Offline
+{
+ /// <summary>
+ /// Specifies type of sync requested for given data.
+ /// </summary>
+ public enum SyncOptions
+ {
+ /// <summary>
+ /// No sync needed for given data.
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// Data should be pulled from firebase.
+ /// </summary>
+ Pull,
+
+ /// <summary>
+ /// Data should be put to firebase.
+ /// </summary>
+ Put,
+
+ /// <summary>
+ /// Data should be patched in firebase.
+ /// </summary>
+ Patch
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Query/AuthQuery.cs b/dsa/FireBase/Query/AuthQuery.cs
new file mode 100644
index 0000000..2cfda3c
--- /dev/null
+++ b/dsa/FireBase/Query/AuthQuery.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace Firebase.Database.Query
+{
+ /// <summary>
+ /// Represents an auth parameter in firebase query, e.g. "?auth=xyz".
+ /// </summary>
+ public class AuthQuery : ParameterQuery
+ {
+ private readonly Func<string> tokenFactory;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthQuery" /> class.
+ /// </summary>
+ /// <param name="parent"> The parent. </param>
+ /// <param name="tokenFactory"> The authentication token factory. </param>
+ /// <param name="client"> The owner. </param>
+ public AuthQuery(FirebaseQuery parent, Func<string> tokenFactory, FirebaseClient client) : base(parent,
+ () => client.Options.AsAccessToken ? "access_token" : "auth", client)
+ {
+ this.tokenFactory = tokenFactory;
+ }
+
+ /// <summary>
+ /// Build the url parameter value of this child.
+ /// </summary>
+ /// <param name="child"> The child of this child. </param>
+ /// <returns> The <see cref="string" />. </returns>
+ protected override string BuildUrlParameter(FirebaseQuery child)
+ {
+ return tokenFactory();
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Query/ChildQuery.cs b/dsa/FireBase/Query/ChildQuery.cs
new file mode 100644
index 0000000..014fe09
--- /dev/null
+++ b/dsa/FireBase/Query/ChildQuery.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace Firebase.Database.Query
+{
+ /// <summary>
+ /// Firebase query which references the child of current node.
+ /// </summary>
+ public class ChildQuery : FirebaseQuery
+ {
+ private readonly Func<string> pathFactory;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ChildQuery" /> class.
+ /// </summary>
+ /// <param name="parent"> The parent. </param>
+ /// <param name="pathFactory"> The path to the child node. </param>
+ /// <param name="client"> The owner. </param>
+ public ChildQuery(FirebaseQuery parent, Func<string> pathFactory, FirebaseClient client)
+ : base(parent, client)
+ {
+ this.pathFactory = pathFactory;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ChildQuery" /> class.
+ /// </summary>
+ /// <param name="client"> The client. </param>
+ /// <param name="pathFactory"> The path to the child node. </param>
+ public ChildQuery(FirebaseClient client, Func<string> pathFactory)
+ : this(null, pathFactory, client)
+ {
+ }
+
+ /// <summary>
+ /// Build the url segment of this child.
+ /// </summary>
+ /// <param name="child"> The child of this child. </param>
+ /// <returns> The <see cref="string" />. </returns>
+ protected override string BuildUrlSegment(FirebaseQuery child)
+ {
+ var s = pathFactory();
+
+ if (s != string.Empty && !s.EndsWith("/")) s += '/';
+
+ if (!(child is ChildQuery)) return s + ".json";
+
+ return s;
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Query/FilterQuery.cs b/dsa/FireBase/Query/FilterQuery.cs
new file mode 100644
index 0000000..3434d1d
--- /dev/null
+++ b/dsa/FireBase/Query/FilterQuery.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Globalization;
+
+namespace Firebase.Database.Query
+{
+ /// <summary>
+ /// Represents a firebase filtering query, e.g. "?LimitToLast=10".
+ /// </summary>
+ public class FilterQuery : ParameterQuery
+ {
+ private readonly Func<bool> boolValueFactory;
+ private readonly Func<double> doubleValueFactory;
+ private readonly Func<string> valueFactory;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FilterQuery" /> class.
+ /// </summary>
+ /// <param name="parent"> The parent. </param>
+ /// <param name="filterFactory"> The filter. </param>
+ /// <param name="valueFactory"> The value for filter. </param>
+ /// <param name="client"> The owning client. </param>
+ public FilterQuery(FirebaseQuery parent, Func<string> filterFactory, Func<string> valueFactory,
+ FirebaseClient client)
+ : base(parent, filterFactory, client)
+ {
+ this.valueFactory = valueFactory;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FilterQuery" /> class.
+ /// </summary>
+ /// <param name="parent"> The parent. </param>
+ /// <param name="filterFactory"> The filter. </param>
+ /// <param name="valueFactory"> The value for filter. </param>
+ /// <param name="client"> The owning client. </param>
+ public FilterQuery(FirebaseQuery parent, Func<string> filterFactory, Func<double> valueFactory,
+ FirebaseClient client)
+ : base(parent, filterFactory, client)
+ {
+ doubleValueFactory = valueFactory;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FilterQuery" /> class.
+ /// </summary>
+ /// <param name="parent"> The parent. </param>
+ /// <param name="filterFactory"> The filter. </param>
+ /// <param name="valueFactory"> The value for filter. </param>
+ /// <param name="client"> The owning client. </param>
+ public FilterQuery(FirebaseQuery parent, Func<string> filterFactory, Func<bool> valueFactory,
+ FirebaseClient client)
+ : base(parent, filterFactory, client)
+ {
+ boolValueFactory = valueFactory;
+ }
+
+ /// <summary>
+ /// The build url parameter.
+ /// </summary>
+ /// <param name="child"> The child. </param>
+ /// <returns> Url parameter part of the resulting path. </returns>
+ protected override string BuildUrlParameter(FirebaseQuery child)
+ {
+ if (valueFactory != null)
+ {
+ if (valueFactory() == null) return "null";
+ return $"\"{valueFactory()}\"";
+ }
+
+ if (doubleValueFactory != null)
+ return doubleValueFactory().ToString(CultureInfo.InvariantCulture);
+ if (boolValueFactory != null) return $"{boolValueFactory().ToString().ToLower()}";
+
+ return string.Empty;
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Query/FirebaseQuery.cs b/dsa/FireBase/Query/FirebaseQuery.cs
new file mode 100644
index 0000000..60d0289
--- /dev/null
+++ b/dsa/FireBase/Query/FirebaseQuery.cs
@@ -0,0 +1,314 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Firebase.Database.Http;
+using Firebase.Database.Streaming;
+using Newtonsoft.Json;
+
+namespace Firebase.Database.Query
+{
+ /// <summary>
+ /// Represents a firebase query.
+ /// </summary>
+ public abstract class FirebaseQuery : IFirebaseQuery, IDisposable
+ {
+ protected readonly FirebaseQuery Parent;
+
+ private HttpClient client;
+ protected TimeSpan DEFAULT_HTTP_CLIENT_TIMEOUT = new TimeSpan(0, 0, 180);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FirebaseQuery" /> class.
+ /// </summary>
+ /// <param name="parent"> The parent of this query. </param>
+ /// <param name="client"> The owning client. </param>
+ protected FirebaseQuery(FirebaseQuery parent, FirebaseClient client)
+ {
+ Client = client;
+ Parent = parent;
+ }
+
+ /// <summary>
+ /// Disposes this instance.
+ /// </summary>
+ public void Dispose()
+ {
+ client?.Dispose();
+ }
+
+ /// <summary>
+ /// Gets the client.
+ /// </summary>
+ public FirebaseClient Client { get; }
+
+ /// <summary>
+ /// Queries the firebase server once returning collection of items.
+ /// </summary>
+ /// <param name="timeout"> Optional timeout value. </param>
+ /// <typeparam name="T"> Type of elements. </typeparam>
+ /// <returns> Collection of <see cref="FirebaseObject{T}" /> holding the entities returned by server. </returns>
+ public async Task<IReadOnlyCollection<FirebaseObject<T>>> OnceAsync<T>(TimeSpan? timeout = null)
+ {
+ var url = string.Empty;
+
+ try
+ {
+ url = await BuildUrlAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ throw new FirebaseException("Couldn't build the url", string.Empty, string.Empty, HttpStatusCode.OK,
+ ex);
+ }
+
+ return await GetClient(timeout).GetObjectCollectionAsync<T>(url, Client.Options.JsonSerializerSettings)
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Starts observing this query watching for changes real time sent by the server.
+ /// </summary>
+ /// <typeparam name="T"> Type of elements. </typeparam>
+ /// <param name="elementRoot"> Optional custom root element of received json items. </param>
+ /// <returns> Observable stream of <see cref="FirebaseEvent{T}" />. </returns>
+ public IObservable<FirebaseEvent<T>> AsObservable<T>(
+ EventHandler<ExceptionEventArgs<FirebaseException>> exceptionHandler = null, string elementRoot = "")
+ {
+ return Observable.Create<FirebaseEvent<T>>(observer =>
+ {
+ var sub = new FirebaseSubscription<T>(observer, this, elementRoot, new FirebaseCache<T>());
+ sub.ExceptionThrown += exceptionHandler;
+ return sub.Run();
+ });
+ }
+
+ /// <summary>
+ /// Builds the actual URL of this query.
+ /// </summary>
+ /// <returns> The <see cref="string" />. </returns>
+ public async Task<string> BuildUrlAsync()
+ {
+ // if token factory is present on the parent then use it to generate auth token
+ if (Client.Options.AuthTokenAsyncFactory != null)
+ {
+ var token = await Client.Options.AuthTokenAsyncFactory().ConfigureAwait(false);
+ return this.WithAuth(token).BuildUrl(null);
+ }
+
+ return BuildUrl(null);
+ }
+
+ /*public async Task<IReadOnlyCollection<FirebaseObject<Object>>> OnceAsync(Type dataType, TimeSpan? timeout = null)
+ {
+ var url = string.Empty;
+
+ try
+ {
+ url = await this.BuildUrlAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ throw new FirebaseException("Couldn't build the url", string.Empty, string.Empty, HttpStatusCode.OK, ex);
+ }
+
+ return await this.GetClient(timeout).GetObjectCollectionAsync(url, Client.Options.JsonSerializerSettings, dataType)
+ .ConfigureAwait(false);
+ }*/
+
+ /// <summary>
+ /// Assumes given query is pointing to a single object of type <typeparamref name="T" /> and retrieves it.
+ /// </summary>
+ /// <param name="timeout"> Optional timeout value. </param>
+ /// <typeparam name="T"> Type of elements. </typeparam>
+ /// <returns> Single object of type <typeparamref name="T" />. </returns>
+ public async Task<T> OnceSingleAsync<T>(TimeSpan? timeout = null)
+ {
+ var responseData = string.Empty;
+ var statusCode = HttpStatusCode.OK;
+ var url = string.Empty;
+
+ try
+ {
+ url = await BuildUrlAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ throw new FirebaseException("Couldn't build the url", string.Empty, responseData, statusCode, ex);
+ }
+
+ try
+ {
+ var response = await GetClient(timeout).GetAsync(url).ConfigureAwait(false);
+ statusCode = response.StatusCode;
+ responseData = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+
+ response.EnsureSuccessStatusCode();
+ response.Dispose();
+
+ return JsonConvert.DeserializeObject<T>(responseData, Client.Options.JsonSerializerSettings);
+ }
+ catch (Exception ex)
+ {
+ throw new FirebaseException(url, string.Empty, responseData, statusCode, ex);
+ }
+ }
+
+ /// <summary>
+ /// Posts given object to repository.
+ /// </summary>
+ /// <param name="obj"> The object. </param>
+ /// <param name="generateKeyOffline"> Specifies whether the key should be generated offline instead of online. </param>
+ /// <param name="timeout"> Optional timeout value. </param>
+ /// <typeparam name="T"> Type of <see cref="obj" /> </typeparam>
+ /// <returns> Resulting firebase object with populated key. </returns>
+ public async Task<FirebaseObject<string>> PostAsync(string data, bool generateKeyOffline = true,
+ TimeSpan? timeout = null)
+ {
+ // post generates a new key server-side, while put can be used with an already generated local key
+ if (generateKeyOffline)
+ {
+ var key = FirebaseKeyGenerator.Next();
+ await new ChildQuery(this, () => key, Client).PutAsync(data).ConfigureAwait(false);
+
+ return new FirebaseObject<string>(key, data);
+ }
+
+ var c = GetClient(timeout);
+ var sendData = await SendAsync(c, data, HttpMethod.Post).ConfigureAwait(false);
+ var result = JsonConvert.DeserializeObject<PostResult>(sendData, Client.Options.JsonSerializerSettings);
+
+ return new FirebaseObject<string>(result.Name, data);
+ }
+
+ /// <summary>
+ /// Patches data at given location instead of overwriting them.
+ /// </summary>
+ /// <param name="obj"> The object. </param>
+ /// <param name="timeout"> Optional timeout value. </param>
+ /// <typeparam name="T"> Type of <see cref="obj" /> </typeparam>
+ /// <returns> The <see cref="Task" />. </returns>
+ public async Task PatchAsync(string data, TimeSpan? timeout = null)
+ {
+ var c = GetClient(timeout);
+
+ await this.Silent().SendAsync(c, data, new HttpMethod("PATCH")).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Sets or overwrites data at given location.
+ /// </summary>
+ /// <param name="obj"> The object. </param>
+ /// <param name="timeout"> Optional timeout value. </param>
+ /// <typeparam name="T"> Type of <see cref="obj" /> </typeparam>
+ /// <returns> The <see cref="Task" />. </returns>
+ public async Task PutAsync(string data, TimeSpan? timeout = null)
+ {
+ var c = GetClient(timeout);
+
+ await this.Silent().SendAsync(c, data, HttpMethod.Put).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Deletes data from given location.
+ /// </summary>
+ /// <param name="timeout"> Optional timeout value. </param>
+ /// <returns> The <see cref="Task" />. </returns>
+ public async Task DeleteAsync(TimeSpan? timeout = null)
+ {
+ var c = GetClient(timeout);
+ var url = string.Empty;
+ var responseData = string.Empty;
+ var statusCode = HttpStatusCode.OK;
+
+ try
+ {
+ url = await BuildUrlAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ throw new FirebaseException("Couldn't build the url", string.Empty, responseData, statusCode, ex);
+ }
+
+ try
+ {
+ var result = await c.DeleteAsync(url).ConfigureAwait(false);
+ statusCode = result.StatusCode;
+ responseData = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
+
+ result.EnsureSuccessStatusCode();
+ }
+ catch (Exception ex)
+ {
+ throw new FirebaseException(url, string.Empty, responseData, statusCode, ex);
+ }
+ }
+
+ /// <summary>
+ /// Build the url segment of this child.
+ /// </summary>
+ /// <param name="child"> The child of this query. </param>
+ /// <returns> The <see cref="string" />. </returns>
+ protected abstract string BuildUrlSegment(FirebaseQuery child);
+
+ private string BuildUrl(FirebaseQuery child)
+ {
+ var url = BuildUrlSegment(child);
+
+ if (Parent != null) url = Parent.BuildUrl(this) + url;
+
+ return url;
+ }
+
+ private HttpClient GetClient(TimeSpan? timeout = null)
+ {
+ if (client == null) client = new HttpClient();
+
+ if (!timeout.HasValue)
+ client.Timeout = DEFAULT_HTTP_CLIENT_TIMEOUT;
+ else
+ client.Timeout = timeout.Value;
+
+ return client;
+ }
+
+ private async Task<string> SendAsync(HttpClient client, string data, HttpMethod method)
+ {
+ var responseData = string.Empty;
+ var statusCode = HttpStatusCode.OK;
+ var requestData = data;
+ var url = string.Empty;
+
+ try
+ {
+ url = await BuildUrlAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ throw new FirebaseException("Couldn't build the url", requestData, responseData, statusCode, ex);
+ }
+
+ var message = new HttpRequestMessage(method, url)
+ {
+ Content = new StringContent(requestData)
+ };
+
+ try
+ {
+ var result = await client.SendAsync(message).ConfigureAwait(false);
+ statusCode = result.StatusCode;
+ responseData = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
+
+ result.EnsureSuccessStatusCode();
+
+ return responseData;
+ }
+ catch (Exception ex)
+ {
+ throw new FirebaseException(url, requestData, responseData, statusCode, ex);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Query/IFirebaseQuery.cs b/dsa/FireBase/Query/IFirebaseQuery.cs
new file mode 100644
index 0000000..0da4b15
--- /dev/null
+++ b/dsa/FireBase/Query/IFirebaseQuery.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Firebase.Database.Streaming;
+
+namespace Firebase.Database.Query
+{
+ /// <summary>
+ /// The FirebaseQuery interface.
+ /// </summary>
+ public interface IFirebaseQuery
+ {
+ /// <summary>
+ /// Gets the owning client of this query.
+ /// </summary>
+ FirebaseClient Client { get; }
+
+ /// <summary>
+ /// Retrieves items which exist on the location specified by this query instance.
+ /// </summary>
+ /// <param name="timeout"> Optional timeout value. </param>
+ /// <typeparam name="T"> Type of the items. </typeparam>
+ /// <returns> Collection of <see cref="FirebaseObject{T}" />. </returns>
+ Task<IReadOnlyCollection<FirebaseObject<T>>> OnceAsync<T>(TimeSpan? timeout = null);
+
+ /// <summary>
+ /// Returns current location as an observable which allows to real-time listening to events from the firebase server.
+ /// </summary>
+ /// <typeparam name="T"> Type of the items. </typeparam>
+ /// <returns> Cold observable of <see cref="FirebaseEvent{T}" />. </returns>
+ IObservable<FirebaseEvent<T>> AsObservable<T>(
+ EventHandler<ExceptionEventArgs<FirebaseException>> exceptionHandler, string elementRoot = "");
+
+ /// <summary>
+ /// Builds the actual url of this query.
+ /// </summary>
+ /// <returns> The <see cref="string" />. </returns>
+ Task<string> BuildUrlAsync();
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Query/OrderQuery.cs b/dsa/FireBase/Query/OrderQuery.cs
new file mode 100644
index 0000000..302d1a3
--- /dev/null
+++ b/dsa/FireBase/Query/OrderQuery.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace Firebase.Database.Query
+{
+ /// <summary>
+ /// Represents a firebase ordering query, e.g. "?OrderBy=Foo".
+ /// </summary>
+ public class OrderQuery : ParameterQuery
+ {
+ private readonly Func<string> propertyNameFactory;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OrderQuery" /> class.
+ /// </summary>
+ /// <param name="parent"> The query parent. </param>
+ /// <param name="propertyNameFactory"> The property name. </param>
+ /// <param name="client"> The owning client. </param>
+ public OrderQuery(ChildQuery parent, Func<string> propertyNameFactory, FirebaseClient client)
+ : base(parent, () => "orderBy", client)
+ {
+ this.propertyNameFactory = propertyNameFactory;
+ }
+
+ /// <summary>
+ /// The build url parameter.
+ /// </summary>
+ /// <param name="child"> The child. </param>
+ /// <returns> The <see cref="string" />. </returns>
+ protected override string BuildUrlParameter(FirebaseQuery child)
+ {
+ return $"\"{propertyNameFactory()}\"";
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Query/ParameterQuery.cs b/dsa/FireBase/Query/ParameterQuery.cs
new file mode 100644
index 0000000..572224c
--- /dev/null
+++ b/dsa/FireBase/Query/ParameterQuery.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace Firebase.Database.Query
+{
+ /// <summary>
+ /// Represents a parameter in firebase query, e.g. "?data=foo".
+ /// </summary>
+ public abstract class ParameterQuery : FirebaseQuery
+ {
+ private readonly Func<string> parameterFactory;
+ private readonly string separator;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ParameterQuery" /> class.
+ /// </summary>
+ /// <param name="parent"> The parent of this query. </param>
+ /// <param name="parameterFactory"> The parameter. </param>
+ /// <param name="client"> The owning client. </param>
+ protected ParameterQuery(FirebaseQuery parent, Func<string> parameterFactory, FirebaseClient client)
+ : base(parent, client)
+ {
+ this.parameterFactory = parameterFactory;
+ separator = Parent is ChildQuery ? "?" : "&";
+ }
+
+ /// <summary>
+ /// Build the url segment represented by this query.
+ /// </summary>
+ /// <param name="child"> The child. </param>
+ /// <returns> The <see cref="string" />. </returns>
+ protected override string BuildUrlSegment(FirebaseQuery child)
+ {
+ return $"{separator}{parameterFactory()}={BuildUrlParameter(child)}";
+ }
+
+ /// <summary>
+ /// The build url parameter.
+ /// </summary>
+ /// <param name="child"> The child. </param>
+ /// <returns> The <see cref="string" />. </returns>
+ protected abstract string BuildUrlParameter(FirebaseQuery child);
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Query/QueryExtensions.cs b/dsa/FireBase/Query/QueryExtensions.cs
new file mode 100644
index 0000000..df2edfc
--- /dev/null
+++ b/dsa/FireBase/Query/QueryExtensions.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace Firebase.Database.Query
+{
+ /// <summary>
+ /// Query extensions providing linq like syntax for firebase server methods.
+ /// </summary>
+ public static class QueryExtensions
+ {
+ /// <summary>
+ /// Adds an auth parameter to the query.
+ /// </summary>
+ /// <param name="node"> The child. </param>
+ /// <param name="token"> The auth token. </param>
+ /// <returns> The <see cref="AuthQuery" />. </returns>
+ internal static AuthQuery WithAuth(this FirebaseQuery node, string token)
+ {
+ return node.WithAuth(() => token);
+ }
+
+ /// <summary>
+ /// Appends print=silent to save bandwidth.
+ /// </summary>
+ /// <param name="node"> The child. </param>
+ /// <returns> The <see cref="SilentQuery" />. </returns>
+ internal static SilentQuery Silent(this FirebaseQuery node)
+ {
+ return new SilentQuery(node, node.Client);
+ }
+
+ /// <summary>
+ /// References a sub child of the existing node.
+ /// </summary>
+ /// <param name="node"> The child. </param>
+ /// <param name="path"> The path of sub child. </param>
+ /// <returns> The <see cref="ChildQuery" />. </returns>
+ public static ChildQuery Child(this ChildQuery node, string path)
+ {
+ return node.Child(() => path);
+ }
+
+ /// <summary>
+ /// Order data by given <see cref="propertyName" />. Note that this is used mainly for following filtering queries and
+ /// due to firebase implementation
+ /// the data may actually not be ordered.
+ /// </summary>
+ /// <param name="child"> The child. </param>
+ /// <param name="propertyName"> The property name. </param>
+ /// <returns> The <see cref="OrderQuery" />. </returns>
+ public static OrderQuery OrderBy(this ChildQuery child, string propertyName)
+ {
+ return child.OrderBy(() => propertyName);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data greater or equal to the <see cref="value" />. This must be preceded by an OrderBy
+ /// query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="value"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery StartAt(this ParameterQuery child, string value)
+ {
+ return child.StartAt(() => value);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data lower or equal to the <see cref="value" />. This must be preceded by an OrderBy
+ /// query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="value"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery EndAt(this ParameterQuery child, string value)
+ {
+ return child.EndAt(() => value);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data equal to the <see cref="value" />. This must be preceded by an OrderBy query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="value"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery EqualTo(this ParameterQuery child, string value)
+ {
+ return child.EqualTo(() => value);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data greater or equal to the <see cref="value" />. This must be preceded by an OrderBy
+ /// query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="value"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery StartAt(this ParameterQuery child, double value)
+ {
+ return child.StartAt(() => value);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data lower or equal to the <see cref="value" />. This must be preceded by an OrderBy
+ /// query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="value"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery EndAt(this ParameterQuery child, double value)
+ {
+ return child.EndAt(() => value);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data equal to the <see cref="value" />. This must be preceded by an OrderBy query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="value"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery EqualTo(this ParameterQuery child, double value)
+ {
+ return child.EqualTo(() => value);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data equal to the <see cref="value" />. This must be preceded by an OrderBy query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="value"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery EqualTo(this ParameterQuery child, bool value)
+ {
+ return child.EqualTo(() => value);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data equal to null. This must be preceded by an OrderBy query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery EqualTo(this ParameterQuery child)
+ {
+ return child.EqualTo(() => null);
+ }
+
+ /// <summary>
+ /// Limits the result to first <see cref="count" /> items.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="count"> Number of elements. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery LimitToFirst(this ParameterQuery child, int count)
+ {
+ return child.LimitToFirst(() => count);
+ }
+
+ /// <summary>
+ /// Limits the result to last <see cref="count" /> items.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="count"> Number of elements. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery LimitToLast(this ParameterQuery child, int count)
+ {
+ return child.LimitToLast(() => count);
+ }
+
+ public static Task PutAsync<T>(this FirebaseQuery query, T obj)
+ {
+ return query.PutAsync(JsonConvert.SerializeObject(obj, query.Client.Options.JsonSerializerSettings));
+ }
+
+ public static Task PatchAsync<T>(this FirebaseQuery query, T obj)
+ {
+ return query.PatchAsync(JsonConvert.SerializeObject(obj, query.Client.Options.JsonSerializerSettings));
+ }
+
+ public static async Task<FirebaseObject<T>> PostAsync<T>(this FirebaseQuery query, T obj,
+ bool generateKeyOffline = true)
+ {
+ var result =
+ await query.PostAsync(JsonConvert.SerializeObject(obj, query.Client.Options.JsonSerializerSettings),
+ generateKeyOffline);
+
+ return new FirebaseObject<T>(result.Key, obj);
+ }
+
+ /// <summary>
+ /// Fan out given item to multiple locations at once. See
+ /// https://firebase.googleblog.com/2015/10/client-side-fan-out-for-data-consistency_73.html for details.
+ /// </summary>
+ /// <typeparam name="T"> Type of object to fan out. </typeparam>
+ /// <param name="query"> Current node. </param>
+ /// <param name="item"> Object to fan out. </param>
+ /// <param name="relativePaths"> Locations where to store the item. </param>
+ public static async Task FanOut<T>(this ChildQuery child, T item, params string[] relativePaths)
+ {
+ if (relativePaths == null) throw new ArgumentNullException(nameof(relativePaths));
+
+ var fanoutObject = new Dictionary<string, T>(relativePaths.Length);
+
+ foreach (var path in relativePaths) fanoutObject.Add(path, item);
+
+ await child.PatchAsync(fanoutObject);
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Query/QueryFactoryExtensions.cs b/dsa/FireBase/Query/QueryFactoryExtensions.cs
new file mode 100644
index 0000000..71dae5c
--- /dev/null
+++ b/dsa/FireBase/Query/QueryFactoryExtensions.cs
@@ -0,0 +1,187 @@
+using System;
+
+namespace Firebase.Database.Query
+{
+ /// <summary>
+ /// Query extensions providing linq like syntax for firebase server methods.
+ /// </summary>
+ public static class QueryFactoryExtensions
+ {
+ /// <summary>
+ /// Adds an auth parameter to the query.
+ /// </summary>
+ /// <param name="node"> The child. </param>
+ /// <param name="tokenFactory"> The auth token. </param>
+ /// <returns> The <see cref="AuthQuery" />. </returns>
+ internal static AuthQuery WithAuth(this FirebaseQuery node, Func<string> tokenFactory)
+ {
+ return new AuthQuery(node, tokenFactory, node.Client);
+ }
+
+ /// <summary>
+ /// References a sub child of the existing node.
+ /// </summary>
+ /// <param name="node"> The child. </param>
+ /// <param name="pathFactory"> The path of sub child. </param>
+ /// <returns> The <see cref="ChildQuery" />. </returns>
+ public static ChildQuery Child(this ChildQuery node, Func<string> pathFactory)
+ {
+ return new ChildQuery(node, pathFactory, node.Client);
+ }
+
+ /// <summary>
+ /// Order data by given <see cref="propertyNameFactory" />. Note that this is used mainly for following filtering
+ /// queries and due to firebase implementation
+ /// the data may actually not be ordered.
+ /// </summary>
+ /// <param name="child"> The child. </param>
+ /// <param name="propertyNameFactory"> The property name. </param>
+ /// <returns> The <see cref="OrderQuery" />. </returns>
+ public static OrderQuery OrderBy(this ChildQuery child, Func<string> propertyNameFactory)
+ {
+ return new OrderQuery(child, propertyNameFactory, child.Client);
+ }
+
+ /// <summary>
+ /// Order data by $key. Note that this is used mainly for following filtering queries and due to firebase
+ /// implementation
+ /// the data may actually not be ordered.
+ /// </summary>
+ /// <param name="child"> The child. </param>
+ /// <returns> The <see cref="OrderQuery" />. </returns>
+ public static OrderQuery OrderByKey(this ChildQuery child)
+ {
+ return child.OrderBy("$key");
+ }
+
+ /// <summary>
+ /// Order data by $value. Note that this is used mainly for following filtering queries and due to firebase
+ /// implementation
+ /// the data may actually not be ordered.
+ /// </summary>
+ /// <param name="child"> The child. </param>
+ /// <returns> The <see cref="OrderQuery" />. </returns>
+ public static OrderQuery OrderByValue(this ChildQuery child)
+ {
+ return child.OrderBy("$value");
+ }
+
+ /// <summary>
+ /// Order data by $priority. Note that this is used mainly for following filtering queries and due to firebase
+ /// implementation
+ /// the data may actually not be ordered.
+ /// </summary>
+ /// <param name="child"> The child. </param>
+ /// <returns> The <see cref="OrderQuery" />. </returns>
+ public static OrderQuery OrderByPriority(this ChildQuery child)
+ {
+ return child.OrderBy("$priority");
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data greater or equal to the <see cref="valueFactory" />. This must be preceded by an
+ /// OrderBy query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="valueFactory"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery StartAt(this ParameterQuery child, Func<string> valueFactory)
+ {
+ return new FilterQuery(child, () => "startAt", valueFactory, child.Client);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data lower or equal to the <see cref="valueFactory" />. This must be preceded by an
+ /// OrderBy query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="valueFactory"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery EndAt(this ParameterQuery child, Func<string> valueFactory)
+ {
+ return new FilterQuery(child, () => "endAt", valueFactory, child.Client);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data equal to the <see cref="valueFactory" />. This must be preceded by an OrderBy
+ /// query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="valueFactory"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery EqualTo(this ParameterQuery child, Func<string> valueFactory)
+ {
+ return new FilterQuery(child, () => "equalTo", valueFactory, child.Client);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data greater or equal to the <see cref="valueFactory" />. This must be preceded by an
+ /// OrderBy query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="valueFactory"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery StartAt(this ParameterQuery child, Func<double> valueFactory)
+ {
+ return new FilterQuery(child, () => "startAt", valueFactory, child.Client);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data lower or equal to the <see cref="valueFactory" />. This must be preceded by an
+ /// OrderBy query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="valueFactory"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery EndAt(this ParameterQuery child, Func<double> valueFactory)
+ {
+ return new FilterQuery(child, () => "endAt", valueFactory, child.Client);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data equal to the <see cref="valueFactory" />. This must be preceded by an OrderBy
+ /// query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="valueFactory"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery EqualTo(this ParameterQuery child, Func<double> valueFactory)
+ {
+ return new FilterQuery(child, () => "equalTo", valueFactory, child.Client);
+ }
+
+ /// <summary>
+ /// Instructs firebase to send data equal to the <see cref="valueFactory" />. This must be preceded by an OrderBy
+ /// query.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="valueFactory"> Value to start at. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery EqualTo(this ParameterQuery child, Func<bool> valueFactory)
+ {
+ return new FilterQuery(child, () => "equalTo", valueFactory, child.Client);
+ }
+
+ /// <summary>
+ /// Limits the result to first <see cref="countFactory" /> items.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="countFactory"> Number of elements. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery LimitToFirst(this ParameterQuery child, Func<int> countFactory)
+ {
+ return new FilterQuery(child, () => "limitToFirst", () => countFactory(), child.Client);
+ }
+
+ /// <summary>
+ /// Limits the result to last <see cref="countFactory" /> items.
+ /// </summary>
+ /// <param name="child"> Current node. </param>
+ /// <param name="countFactory"> Number of elements. </param>
+ /// <returns> The <see cref="FilterQuery" />. </returns>
+ public static FilterQuery LimitToLast(this ParameterQuery child, Func<int> countFactory)
+ {
+ return new FilterQuery(child, () => "limitToLast", () => countFactory(), child.Client);
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Query/SilentQuery.cs b/dsa/FireBase/Query/SilentQuery.cs
new file mode 100644
index 0000000..d09d38b
--- /dev/null
+++ b/dsa/FireBase/Query/SilentQuery.cs
@@ -0,0 +1,18 @@
+namespace Firebase.Database.Query
+{
+ /// <summary>
+ /// Appends print=silent to the url.
+ /// </summary>
+ public class SilentQuery : ParameterQuery
+ {
+ public SilentQuery(FirebaseQuery parent, FirebaseClient client)
+ : base(parent, () => "print", client)
+ {
+ }
+
+ protected override string BuildUrlParameter(FirebaseQuery child)
+ {
+ return "silent";
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Settings.StyleCop b/dsa/FireBase/Settings.StyleCop
new file mode 100644
index 0000000..833aa39
--- /dev/null
+++ b/dsa/FireBase/Settings.StyleCop
@@ -0,0 +1,77 @@
+<StyleCopSettings Version="105">
+ <GlobalSettings>
+ <CollectionProperty Name="RecognizedWords">
+ <Value>auth</Value>
+ <Value>firebase</Value>
+ <Value>json</Value>
+ <Value>linq</Value>
+ <Value>oauth</Value>
+ </CollectionProperty>
+ </GlobalSettings>
+ <Analyzers>
+ <Analyzer AnalyzerId="StyleCop.CSharp.DocumentationRules">
+ <Rules>
+ <Rule Name="FileMustHaveHeader">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderMustShowCopyright">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderMustHaveCopyrightText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderMustContainFileName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderFileNameDocumentationMustMatchFileName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderMustHaveValidCompanyText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderFileNameDocumentationMustMatchTypeName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationTextMustBeginWithACapitalLetter">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">True</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationTextMustEndWithAPeriod">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">True</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ <AnalyzerSettings>
+ <BooleanProperty Name="IgnorePrivates">True</BooleanProperty>
+ <BooleanProperty Name="IgnoreInternals">True</BooleanProperty>
+ <BooleanProperty Name="IncludeFields">False</BooleanProperty>
+ </AnalyzerSettings>
+ </Analyzer>
+ <Analyzer AnalyzerId="StyleCop.CSharp.ReadabilityRules">
+ <Rules>
+ <Rule Name="DoNotUseRegions">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">True</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ <AnalyzerSettings />
+ </Analyzer>
+ </Analyzers>
+</StyleCopSettings> \ No newline at end of file
diff --git a/dsa/FireBase/Streaming/FirebaseCache.cs b/dsa/FireBase/Streaming/FirebaseCache.cs
new file mode 100644
index 0000000..66241e0
--- /dev/null
+++ b/dsa/FireBase/Streaming/FirebaseCache.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Firebase.Database.Http;
+using Newtonsoft.Json;
+
+namespace Firebase.Database.Streaming
+{
+ /// <summary>
+ /// The firebase cache.
+ /// </summary>
+ /// <typeparam name="T"> Type of top-level entities in the cache. </typeparam>
+ public class FirebaseCache<T> : IEnumerable<FirebaseObject<T>>
+ {
+ private readonly IDictionary<string, T> dictionary;
+ private readonly bool isDictionaryType;
+
+ private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings
+ {
+ ObjectCreationHandling = ObjectCreationHandling.Replace
+ };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FirebaseCache{T}" /> class.
+ /// </summary>
+ public FirebaseCache()
+ : this(new Dictionary<string, T>())
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FirebaseCache{T}" /> class and populates it with existing data.
+ /// </summary>
+ /// <param name="existingItems"> The existing items. </param>
+ public FirebaseCache(IDictionary<string, T> existingItems)
+ {
+ dictionary = existingItems;
+ isDictionaryType = typeof(IDictionary).GetTypeInfo().IsAssignableFrom(typeof(T).GetTypeInfo());
+ }
+
+ /// <summary>
+ /// The push data.
+ /// </summary>
+ /// <param name="path"> The path of incoming data, separated by slash. </param>
+ /// <param name="data"> The data in json format as returned by firebase. </param>
+ /// <returns> Collection of top-level entities which were affected by the push. </returns>
+ public IEnumerable<FirebaseObject<T>> PushData(string path, string data, bool removeEmptyEntries = true)
+ {
+ object obj = this.dictionary;
+ Action<object> primitiveObjSetter = null;
+ Action objDeleter = null;
+
+ var pathElements = path.Split(new[] {"/"},
+ removeEmptyEntries ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None);
+
+ // first find where we should insert the data to
+ foreach (var element in pathElements)
+ if (obj is IDictionary)
+ {
+ // if it's a dictionary, then it's just a matter of inserting into it / accessing existing object by key
+ var dictionary = obj as IDictionary;
+ var valueType = obj.GetType().GenericTypeArguments[1];
+
+ primitiveObjSetter = d => dictionary[element] = d;
+ objDeleter = () => dictionary.Remove(element);
+
+ if (dictionary.Contains(element))
+ {
+ obj = dictionary[element];
+ }
+ else
+ {
+ dictionary[element] = CreateInstance(valueType);
+ obj = dictionary[element];
+ }
+ }
+ else
+ {
+ // if it's not a dictionary, try to find the property of current object with the matching name
+ var objParent = obj;
+ var property = objParent
+ .GetType()
+ .GetRuntimeProperties()
+ .First(p => p.Name.Equals(element, StringComparison.OrdinalIgnoreCase) ||
+ element == p.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName);
+
+ objDeleter = () => property.SetValue(objParent, null);
+ primitiveObjSetter = d => property.SetValue(objParent, d);
+ obj = property.GetValue(obj);
+ if (obj == null)
+ {
+ obj = CreateInstance(property.PropertyType);
+ property.SetValue(objParent, obj);
+ }
+ }
+
+ // if data is null (=empty string) delete it
+ if (string.IsNullOrWhiteSpace(data) || data == "null")
+ {
+ var key = pathElements[0];
+ var target = dictionary[key];
+
+ objDeleter();
+
+ yield return new FirebaseObject<T>(key, target);
+ yield break;
+ }
+
+ // now insert the data
+ if (obj is IDictionary && !isDictionaryType)
+ {
+ // insert data into dictionary and return it as a collection of FirebaseObject
+ var dictionary = obj as IDictionary;
+ var valueType = obj.GetType().GenericTypeArguments[1];
+ var objectCollection = data.GetObjectCollection(valueType);
+
+ foreach (var item in objectCollection)
+ {
+ dictionary[item.Key] = item.Object;
+
+ // top level dictionary changed
+ if (!pathElements.Any()) yield return new FirebaseObject<T>(item.Key, (T) item.Object);
+ }
+
+ // nested dictionary changed
+ if (pathElements.Any())
+ {
+ this.dictionary[pathElements[0]] = this.dictionary[pathElements[0]];
+ yield return new FirebaseObject<T>(pathElements[0], this.dictionary[pathElements[0]]);
+ }
+ }
+ else
+ {
+ // set the data on a property of the given object
+ var valueType = obj.GetType();
+
+ // firebase sends strings without double quotes
+ var targetObject = valueType == typeof(string)
+ ? data
+ : JsonConvert.DeserializeObject(data, valueType);
+
+ if ((valueType.GetTypeInfo().IsPrimitive || valueType == typeof(string)) && primitiveObjSetter != null)
+ // handle primitive (value) types separately
+ primitiveObjSetter(targetObject);
+ else
+ JsonConvert.PopulateObject(data, obj, serializerSettings);
+
+ dictionary[pathElements[0]] = dictionary[pathElements[0]];
+ yield return new FirebaseObject<T>(pathElements[0], dictionary[pathElements[0]]);
+ }
+ }
+
+ public bool Contains(string key)
+ {
+ return dictionary.Keys.Contains(key);
+ }
+
+ private object CreateInstance(Type type)
+ {
+ if (type == typeof(string))
+ return string.Empty;
+ return Activator.CreateInstance(type);
+ }
+
+ #region IEnumerable
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public IEnumerator<FirebaseObject<T>> GetEnumerator()
+ {
+ return dictionary.Select(p => new FirebaseObject<T>(p.Key, p.Value)).GetEnumerator();
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Streaming/FirebaseEvent.cs b/dsa/FireBase/Streaming/FirebaseEvent.cs
new file mode 100644
index 0000000..1761a72
--- /dev/null
+++ b/dsa/FireBase/Streaming/FirebaseEvent.cs
@@ -0,0 +1,37 @@
+namespace Firebase.Database.Streaming
+{
+ /// <summary>
+ /// Firebase event which hold <see cref="EventType" /> and the object affected by the event.
+ /// </summary>
+ /// <typeparam name="T"> Type of object affected by the event. </typeparam>
+ public class FirebaseEvent<T> : FirebaseObject<T>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FirebaseEvent{T}" /> class.
+ /// </summary>
+ /// <param name="key"> The key of the object. </param>
+ /// <param name="obj"> The object. </param>
+ /// <param name="eventType"> The event type. </param>
+ public FirebaseEvent(string key, T obj, FirebaseEventType eventType, FirebaseEventSource eventSource)
+ : base(key, obj)
+ {
+ EventType = eventType;
+ EventSource = eventSource;
+ }
+
+ /// <summary>
+ /// Gets the source of the event.
+ /// </summary>
+ public FirebaseEventSource EventSource { get; }
+
+ /// <summary>
+ /// Gets the event type.
+ /// </summary>
+ public FirebaseEventType EventType { get; }
+
+ public static FirebaseEvent<T> Empty(FirebaseEventSource source)
+ {
+ return new FirebaseEvent<T>(string.Empty, default(T), FirebaseEventType.InsertOrUpdate, source);
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Streaming/FirebaseEventSource.cs b/dsa/FireBase/Streaming/FirebaseEventSource.cs
new file mode 100644
index 0000000..b1385ca
--- /dev/null
+++ b/dsa/FireBase/Streaming/FirebaseEventSource.cs
@@ -0,0 +1,38 @@
+namespace Firebase.Database.Streaming
+{
+ /// <summary>
+ /// Specifies the origin of given <see cref="FirebaseEvent{T}" />
+ /// </summary>
+ public enum FirebaseEventSource
+ {
+ /// <summary>
+ /// Event comes from an offline source.
+ /// </summary>
+ Offline,
+
+ /// <summary>
+ /// Event comes from online source fetched during initial pull (valid only for RealtimeDatabase).
+ /// </summary>
+ OnlineInitial,
+
+ /// <summary>
+ /// Event comes from online source received thru active stream.
+ /// </summary>
+ OnlineStream,
+
+ /// <summary>
+ /// Event comes from online source being fetched manually.
+ /// </summary>
+ OnlinePull,
+
+ /// <summary>
+ /// Event raised after successful online push (valid only for RealtimeDatabase which isn't streaming).
+ /// </summary>
+ OnlinePush,
+
+ /// <summary>
+ /// Event comes from an online source.
+ /// </summary>
+ Online = OnlineInitial | OnlinePull | OnlinePush | OnlineStream
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Streaming/FirebaseEventType.cs b/dsa/FireBase/Streaming/FirebaseEventType.cs
new file mode 100644
index 0000000..7606331
--- /dev/null
+++ b/dsa/FireBase/Streaming/FirebaseEventType.cs
@@ -0,0 +1,18 @@
+namespace Firebase.Database.Streaming
+{
+ /// <summary>
+ /// The type of event.
+ /// </summary>
+ public enum FirebaseEventType
+ {
+ /// <summary>
+ /// Item was inserted or updated.
+ /// </summary>
+ InsertOrUpdate,
+
+ /// <summary>
+ /// Item was deleted.
+ /// </summary>
+ Delete
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Streaming/FirebaseServerEventType.cs b/dsa/FireBase/Streaming/FirebaseServerEventType.cs
new file mode 100644
index 0000000..79c816d
--- /dev/null
+++ b/dsa/FireBase/Streaming/FirebaseServerEventType.cs
@@ -0,0 +1,15 @@
+namespace Firebase.Database.Streaming
+{
+ internal enum FirebaseServerEventType
+ {
+ Put,
+
+ Patch,
+
+ KeepAlive,
+
+ Cancel,
+
+ AuthRevoked
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Streaming/FirebaseSubscription.cs b/dsa/FireBase/Streaming/FirebaseSubscription.cs
new file mode 100644
index 0000000..fb0f403
--- /dev/null
+++ b/dsa/FireBase/Streaming/FirebaseSubscription.cs
@@ -0,0 +1,217 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using Firebase.Database.Query;
+using Newtonsoft.Json.Linq;
+
+namespace Firebase.Database.Streaming
+{
+ /// <summary>
+ /// The firebase subscription.
+ /// </summary>
+ /// <typeparam name="T"> Type of object to be streaming back to the called. </typeparam>
+ internal class FirebaseSubscription<T> : IDisposable
+ {
+ private static readonly HttpClient http;
+ private readonly FirebaseCache<T> cache;
+ private readonly CancellationTokenSource cancel;
+ private readonly FirebaseClient client;
+ private readonly string elementRoot;
+ private readonly IObserver<FirebaseEvent<T>> observer;
+ private readonly IFirebaseQuery query;
+
+ static FirebaseSubscription()
+ {
+ var handler = new HttpClientHandler
+ {
+ AllowAutoRedirect = true,
+ MaxAutomaticRedirections = 10,
+ CookieContainer = new CookieContainer()
+ };
+
+ var httpClient = new HttpClient(handler, true);
+
+ httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
+
+ http = httpClient;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FirebaseSubscription{T}" /> class.
+ /// </summary>
+ /// <param name="observer"> The observer. </param>
+ /// <param name="query"> The query. </param>
+ /// <param name="cache"> The cache. </param>
+ public FirebaseSubscription(IObserver<FirebaseEvent<T>> observer, IFirebaseQuery query, string elementRoot,
+ FirebaseCache<T> cache)
+ {
+ this.observer = observer;
+ this.query = query;
+ this.elementRoot = elementRoot;
+ cancel = new CancellationTokenSource();
+ this.cache = cache;
+ client = query.Client;
+ }
+
+ public void Dispose()
+ {
+ cancel.Cancel();
+ }
+
+ public event EventHandler<ExceptionEventArgs<FirebaseException>> ExceptionThrown;
+
+ public IDisposable Run()
+ {
+ Task.Run(() => ReceiveThread());
+
+ return this;
+ }
+
+ private async void ReceiveThread()
+ {
+ while (true)
+ {
+ var url = string.Empty;
+ var line = string.Empty;
+ var statusCode = HttpStatusCode.OK;
+
+ try
+ {
+ cancel.Token.ThrowIfCancellationRequested();
+
+ // initialize network connection
+ url = await query.BuildUrlAsync().ConfigureAwait(false);
+ var request = new HttpRequestMessage(HttpMethod.Get, url);
+ var serverEvent = FirebaseServerEventType.KeepAlive;
+
+ var client = GetHttpClient();
+ var response = await client
+ .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancel.Token)
+ .ConfigureAwait(false);
+
+ statusCode = response.StatusCode;
+ response.EnsureSuccessStatusCode();
+
+ using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
+ using (var reader = this.client.Options.SubscriptionStreamReaderFactory(stream))
+ {
+ while (true)
+ {
+ cancel.Token.ThrowIfCancellationRequested();
+
+ line = reader.ReadLine()?.Trim();
+
+ if (string.IsNullOrWhiteSpace(line)) continue;
+
+ var tuple = line.Split(new[] {':'}, 2, StringSplitOptions.RemoveEmptyEntries)
+ .Select(s => s.Trim()).ToArray();
+
+ switch (tuple[0].ToLower())
+ {
+ case "event":
+ serverEvent = ParseServerEvent(serverEvent, tuple[1]);
+ break;
+ case "data":
+ ProcessServerData(url, serverEvent, tuple[1]);
+ break;
+ }
+
+ if (serverEvent == FirebaseServerEventType.AuthRevoked)
+ // auth token no longer valid, reconnect
+ break;
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex) when (statusCode != HttpStatusCode.OK)
+ {
+ observer.OnError(new FirebaseException(url, string.Empty, line, statusCode, ex));
+ Dispose();
+ break;
+ }
+ catch (Exception ex)
+ {
+ ExceptionThrown?.Invoke(this,
+ new ExceptionEventArgs<FirebaseException>(new FirebaseException(url, string.Empty, line,
+ statusCode, ex)));
+
+ await Task.Delay(2000).ConfigureAwait(false);
+ }
+ }
+ }
+
+ private FirebaseServerEventType ParseServerEvent(FirebaseServerEventType serverEvent, string eventName)
+ {
+ switch (eventName)
+ {
+ case "put":
+ serverEvent = FirebaseServerEventType.Put;
+ break;
+ case "patch":
+ serverEvent = FirebaseServerEventType.Patch;
+ break;
+ case "keep-alive":
+ serverEvent = FirebaseServerEventType.KeepAlive;
+ break;
+ case "cancel":
+ serverEvent = FirebaseServerEventType.Cancel;
+ break;
+ case "auth_revoked":
+ serverEvent = FirebaseServerEventType.AuthRevoked;
+ break;
+ }
+
+ return serverEvent;
+ }
+
+ private void ProcessServerData(string url, FirebaseServerEventType serverEvent, string serverData)
+ {
+ switch (serverEvent)
+ {
+ case FirebaseServerEventType.Put:
+ case FirebaseServerEventType.Patch:
+ var result = JObject.Parse(serverData);
+ var path = result["path"].ToString();
+ var data = result["data"].ToString();
+
+ // If an elementRoot parameter is provided, but it's not in the cache, it was already deleted. So we can return an empty object.
+ if (string.IsNullOrWhiteSpace(elementRoot) || !cache.Contains(elementRoot))
+ if (path == "/" && data == string.Empty)
+ {
+ observer.OnNext(FirebaseEvent<T>.Empty(FirebaseEventSource.OnlineStream));
+ return;
+ }
+
+ var eventType = string.IsNullOrWhiteSpace(data)
+ ? FirebaseEventType.Delete
+ : FirebaseEventType.InsertOrUpdate;
+
+ var items = cache.PushData(elementRoot + path, data);
+
+ foreach (var i in items.ToList())
+ observer.OnNext(new FirebaseEvent<T>(i.Key, i.Object, eventType,
+ FirebaseEventSource.OnlineStream));
+
+ break;
+ case FirebaseServerEventType.KeepAlive:
+ break;
+ case FirebaseServerEventType.Cancel:
+ observer.OnError(new FirebaseException(url, string.Empty, serverData, HttpStatusCode.Unauthorized));
+ Dispose();
+ break;
+ }
+ }
+
+ private HttpClient GetHttpClient()
+ {
+ return http;
+ }
+ }
+} \ No newline at end of file
diff --git a/dsa/FireBase/Streaming/NonBlockingStreamReader.cs b/dsa/FireBase/Streaming/NonBlockingStreamReader.cs
new file mode 100644
index 0000000..8228e32
--- /dev/null
+++ b/dsa/FireBase/Streaming/NonBlockingStreamReader.cs
@@ -0,0 +1,63 @@
+using System.IO;
+using System.Text;
+
+namespace Firebase.Database.Streaming
+{
+ /// <summary>
+ /// When a regular <see cref="StreamReader" /> is used in a UWP app its <see cref="StreamReader.ReadLine" /> method
+ /// tends to take a long
+ /// time for data larger then 2 KB. This extremly simple implementation of <see cref="TextReader" /> can be used
+ /// instead to boost performance
+ /// in your UWP app. Use <see cref="FirebaseOptions" /> to inject an instance of this class into your
+ /// <see cref="FirebaseClient" />.
+ /// </summary>
+ public class NonBlockingStreamReader : TextReader
+ {
+ private const int DefaultBufferSize = 16000;
+ private readonly byte[] buffer;
+ private readonly int bufferSize;
+
+ private readonly Stream stream;
+
+ private string cachedData;
+
+ public NonBlockingStreamReader(Stream stream, int bufferSize = DefaultBufferSize)
+ {
+ this.stream = stream;
+ this.bufferSize = bufferSize;
+ buffer = new byte[bufferSize];
+
+ cachedData = string.Empty;
+ }
+
+ public override string ReadLine()
+ {
+ var currentString = TryGetNewLine();
+
+ while (currentString == null)
+ {
+ var read = stream.Read(buffer, 0, bufferSize);
+ var str = Encoding.UTF8.GetString(buffer, 0, read);
+
+ cachedData += str;
+ currentString = TryGetNewLine();
+ }
+
+ return currentString;
+ }
+
+ private string TryGetNewLine()
+ {
+ var newLine = cachedData.IndexOf('\n');
+
+ if (newLine >= 0)
+ {
+ var r = cachedData.Substring(0, newLine + 1);
+ cachedData = cachedData.Remove(0, r.Length);
+ return r.Trim();
+ }
+
+ return null;
+ }
+ }
+} \ No newline at end of file