summaryrefslogtreecommitdiff
path: root/FireBase
diff options
context:
space:
mode:
authorTrueDoctor <d-kobert@web.de>2018-09-29 16:51:26 +0200
committerTrueDoctor <d-kobert@web.de>2018-09-29 16:51:26 +0200
commitde0f076ef9ff546c9a90513259ad6c42cd2224b3 (patch)
tree995d766417fd2093d7950694ef17f3c84b7f1042 /FireBase
parent04912687127303fd270b61f131cf68fd4aaae956 (diff)
added firebase api
Diffstat (limited to 'FireBase')
-rw-r--r--FireBase/ExceptionEventArgs.cs28
-rw-r--r--FireBase/Extensions/ObservableExtensions.cs40
-rw-r--r--FireBase/Extensions/TaskExtensions.cs23
-rw-r--r--FireBase/FireBase.csproj13
-rw-r--r--FireBase/FirebaseClient.cs57
-rw-r--r--FireBase/FirebaseException.cs63
-rw-r--r--FireBase/FirebaseKeyGenerator.cs92
-rw-r--r--FireBase/FirebaseObject.cs31
-rw-r--r--FireBase/FirebaseOptions.cs76
-rw-r--r--FireBase/Http/HttpClientExtensions.cs90
-rw-r--r--FireBase/Http/PostResult.cs17
-rw-r--r--FireBase/ObservableExtensions.cs44
-rw-r--r--FireBase/Offline/ConcurrentOfflineDatabase.cs207
-rw-r--r--FireBase/Offline/DatabaseExtensions.cs195
-rw-r--r--FireBase/Offline/ISetHandler.cs11
-rw-r--r--FireBase/Offline/InitialPullStrategy.cs23
-rw-r--r--FireBase/Offline/Internals/MemberAccessVisitor.cs51
-rw-r--r--FireBase/Offline/OfflineCacheAdapter.cs165
-rw-r--r--FireBase/Offline/OfflineDatabase.cs201
-rw-r--r--FireBase/Offline/OfflineEntry.cs116
-rw-r--r--FireBase/Offline/RealtimeDatabase.cs459
-rw-r--r--FireBase/Offline/SetHandler.cs24
-rw-r--r--FireBase/Offline/StreamingOptions.cs21
-rw-r--r--FireBase/Offline/SyncOptions.cs28
-rw-r--r--FireBase/Query/AuthQuery.cs33
-rw-r--r--FireBase/Query/ChildQuery.cs56
-rw-r--r--FireBase/Query/FilterQuery.cs81
-rw-r--r--FireBase/Query/FirebaseQuery.cs314
-rw-r--r--FireBase/Query/IFirebaseQuery.cs43
-rw-r--r--FireBase/Query/OrderQuery.cs34
-rw-r--r--FireBase/Query/ParameterQuery.cs43
-rw-r--r--FireBase/Query/QueryExtensions.cs207
-rw-r--r--FireBase/Query/QueryFactoryExtensions.cs176
-rw-r--r--FireBase/Query/SilentQuery.cs18
-rw-r--r--FireBase/Settings.StyleCop77
-rw-r--r--FireBase/Streaming/FirebaseCache.cs192
-rw-r--r--FireBase/Streaming/FirebaseEvent.cs40
-rw-r--r--FireBase/Streaming/FirebaseEventSource.cs38
-rw-r--r--FireBase/Streaming/FirebaseEventType.cs18
-rw-r--r--FireBase/Streaming/FirebaseServerEventType.cs15
-rw-r--r--FireBase/Streaming/FirebaseSubscription.cs221
-rw-r--r--FireBase/Streaming/NonBlockingStreamReader.cs60
42 files changed, 3741 insertions, 0 deletions
diff --git a/FireBase/ExceptionEventArgs.cs b/FireBase/ExceptionEventArgs.cs
new file mode 100644
index 0000000..f1c7fac
--- /dev/null
+++ b/FireBase/ExceptionEventArgs.cs
@@ -0,0 +1,28 @@
+namespace Firebase.Database
+{
+ using System;
+
+ /// <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)
+ {
+ this.Exception = exception;
+ }
+ }
+
+ public class ExceptionEventArgs : ExceptionEventArgs<Exception>
+ {
+ public ExceptionEventArgs(Exception exception) : base(exception)
+ {
+ }
+ }
+}
diff --git a/FireBase/Extensions/ObservableExtensions.cs b/FireBase/Extensions/ObservableExtensions.cs
new file mode 100644
index 0000000..12cd5f3
--- /dev/null
+++ b/FireBase/Extensions/ObservableExtensions.cs
@@ -0,0 +1,40 @@
+namespace Firebase.Database.Extensions
+{
+ using System;
+ using System.Reactive.Linq;
+
+ 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
+ {
+ int 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));
+ }
+ }
+}
diff --git a/FireBase/Extensions/TaskExtensions.cs b/FireBase/Extensions/TaskExtensions.cs
new file mode 100644
index 0000000..26bbde6
--- /dev/null
+++ b/FireBase/Extensions/TaskExtensions.cs
@@ -0,0 +1,23 @@
+namespace Firebase.Database.Extensions
+{
+ using System;
+ using System.Threading.Tasks;
+
+ 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;
+ }
+ }
+ }
+}
diff --git a/FireBase/FireBase.csproj b/FireBase/FireBase.csproj
new file mode 100644
index 0000000..889c32f
--- /dev/null
+++ b/FireBase/FireBase.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp2.1</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/FireBase/FirebaseClient.cs b/FireBase/FirebaseClient.cs
new file mode 100644
index 0000000..a237c8d
--- /dev/null
+++ b/FireBase/FirebaseClient.cs
@@ -0,0 +1,57 @@
+using System.Net.Http;
+
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Firebase.Database.Tests")]
+
+namespace Firebase.Database
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+
+ using Firebase.Database.Offline;
+ using Firebase.Database.Query;
+
+ /// <summary>
+ /// Firebase client which acts as an entry point to the online database.
+ /// </summary>
+ public class FirebaseClient : IDisposable
+ {
+ internal readonly HttpClient HttpClient;
+ internal readonly FirebaseOptions Options;
+
+ private readonly string baseUrl;
+
+ /// <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)
+ {
+ this.HttpClient = new HttpClient();
+ this.Options = options ?? new FirebaseOptions();
+
+ this.baseUrl = baseUrl;
+
+ if (!this.baseUrl.EndsWith("/"))
+ {
+ this.baseUrl += "/";
+ }
+ }
+
+ /// <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, () => this.baseUrl + resourceName);
+ }
+
+ public void Dispose()
+ {
+ HttpClient?.Dispose();
+ }
+ }
+}
diff --git a/FireBase/FirebaseException.cs b/FireBase/FirebaseException.cs
new file mode 100644
index 0000000..e4b782b
--- /dev/null
+++ b/FireBase/FirebaseException.cs
@@ -0,0 +1,63 @@
+namespace Firebase.Database
+{
+ using System;
+ using System.Net;
+
+ public class FirebaseException : Exception
+ {
+ public FirebaseException(string requestUrl, string requestData, string responseData, HttpStatusCode statusCode)
+ : base(GenerateExceptionMessage(requestUrl, requestData, responseData))
+ {
+ this.RequestUrl = requestUrl;
+ this.RequestData = requestData;
+ this.ResponseData = responseData;
+ this.StatusCode = statusCode;
+ }
+
+ public FirebaseException(string requestUrl, string requestData, string responseData, HttpStatusCode statusCode, Exception innerException)
+ : base(GenerateExceptionMessage(requestUrl, requestData, responseData), innerException)
+ {
+ this.RequestUrl = requestUrl;
+ this.RequestData = requestData;
+ this.ResponseData = responseData;
+ this.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}";
+ }
+ }
+}
diff --git a/FireBase/FirebaseKeyGenerator.cs b/FireBase/FirebaseKeyGenerator.cs
new file mode 100644
index 0000000..acad399
--- /dev/null
+++ b/FireBase/FirebaseKeyGenerator.cs
@@ -0,0 +1,92 @@
+namespace Firebase.Database
+{
+ using System;
+ using System.Text;
+
+ /// <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 (int 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 (int 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 (int i = 0; i < 12; i++)
+ {
+ id.Append(PushChars[lastRandChars[i]]);
+ }
+
+ if (id.Length != 20)
+ {
+ throw new Exception("Length should be 20.");
+ }
+
+ return id.ToString();
+ }
+ }
+}
diff --git a/FireBase/FirebaseObject.cs b/FireBase/FirebaseObject.cs
new file mode 100644
index 0000000..ea61893
--- /dev/null
+++ b/FireBase/FirebaseObject.cs
@@ -0,0 +1,31 @@
+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)
+ {
+ this.Key = key;
+ this.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;
+ }
+ }
+}
diff --git a/FireBase/FirebaseOptions.cs b/FireBase/FirebaseOptions.cs
new file mode 100644
index 0000000..9905956
--- /dev/null
+++ b/FireBase/FirebaseOptions.cs
@@ -0,0 +1,76 @@
+namespace Firebase.Database
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Threading.Tasks;
+
+ using Firebase.Database.Offline;
+
+ using Newtonsoft.Json;
+
+ public class FirebaseOptions
+ {
+ public FirebaseOptions()
+ {
+ this.OfflineDatabaseFactory = (t, s) => new Dictionary<string, OfflineEntry>();
+ this.SubscriptionStreamReaderFactory = s => new StreamReader(s);
+ this.JsonSerializerSettings = new JsonSerializerSettings();
+ this.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;
+ }
+ }
+}
diff --git a/FireBase/Http/HttpClientExtensions.cs b/FireBase/Http/HttpClientExtensions.cs
new file mode 100644
index 0000000..444145b
--- /dev/null
+++ b/FireBase/Http/HttpClientExtensions.cs
@@ -0,0 +1,90 @@
+namespace Firebase.Database.Http
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Net.Http;
+ using System.Threading.Tasks;
+
+ using Newtonsoft.Json;
+ using System.Net;
+
+ /// <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="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);
+ }
+ }
+ }
+}
diff --git a/FireBase/Http/PostResult.cs b/FireBase/Http/PostResult.cs
new file mode 100644
index 0000000..3f010d4
--- /dev/null
+++ b/FireBase/Http/PostResult.cs
@@ -0,0 +1,17 @@
+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;
+ }
+ }
+}
diff --git a/FireBase/ObservableExtensions.cs b/FireBase/ObservableExtensions.cs
new file mode 100644
index 0000000..37c3ef7
--- /dev/null
+++ b/FireBase/ObservableExtensions.cs
@@ -0,0 +1,44 @@
+namespace Firebase.Database
+{
+ using System;
+ using System.Collections.ObjectModel;
+
+ using Firebase.Database.Streaming;
+
+ /// <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;
+ }
+ }
+}
diff --git a/FireBase/Offline/ConcurrentOfflineDatabase.cs b/FireBase/Offline/ConcurrentOfflineDatabase.cs
new file mode 100644
index 0000000..226892d
--- /dev/null
+++ b/FireBase/Offline/ConcurrentOfflineDatabase.cs
@@ -0,0 +1,207 @@
+namespace Firebase.Database.Offline
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Concurrent;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+
+ using LiteDB;
+
+ /// <summary>
+ /// The offline database.
+ /// </summary>
+ public class ConcurrentOfflineDatabase : IDictionary<string, OfflineEntry>
+ {
+ private readonly LiteRepository db;
+ private readonly ConcurrentDictionary<string, OfflineEntry> ccache;
+
+ /// <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 = this.GetFileName(itemType.ToString());
+ if(fullName.Length > 100)
+ {
+ fullName = fullName.Substring(0, 100);
+ }
+
+ BsonMapper mapper = BsonMapper.Global;
+ mapper.Entity<OfflineEntry>().Id(o => o.Key);
+
+ string root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ string filename = fullName + filenameModifier + ".db";
+ var path = Path.Combine(root, filename);
+ this.db = new LiteRepository(new LiteDatabase(path, mapper));
+
+ var cache = db.Database
+ .GetCollection<OfflineEntry>()
+ .FindAll()
+ .ToDictionary(o => o.Key, o => o);
+
+ this.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 => this.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 => this.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 => this.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
+ {
+ return this.ccache[key];
+ }
+
+ set
+ {
+ this.ccache.AddOrUpdate(key, value, (k, existing) => value);
+ this.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 this.ccache.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.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)
+ {
+ this.Add(item.Key, item.Value);
+ }
+
+ /// <summary>
+ /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </summary>
+ public void Clear()
+ {
+ this.ccache.Clear();
+ this.db.Delete<OfflineEntry>(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 this.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)
+ {
+ this.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 this.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 this.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)
+ {
+ this.ccache.AddOrUpdate(key, value, (k, existing) => value);
+ this.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)
+ {
+ this.ccache.TryRemove(key, out OfflineEntry _);
+ return this.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 this.ccache.TryGetValue(key, out value);
+ }
+
+ private string GetFileName(string fileName)
+ {
+ var invalidChars = new[] { '`', '[', ',', '=' };
+ foreach(char c in invalidChars.Concat(System.IO.Path.GetInvalidFileNameChars()).Distinct())
+ {
+ fileName = fileName.Replace(c, '_');
+ }
+
+ return fileName;
+ }
+ }
+}
diff --git a/FireBase/Offline/DatabaseExtensions.cs b/FireBase/Offline/DatabaseExtensions.cs
new file mode 100644
index 0000000..4b04314
--- /dev/null
+++ b/FireBase/Offline/DatabaseExtensions.cs
@@ -0,0 +1,195 @@
+namespace Firebase.Database.Offline
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Linq.Expressions;
+ using System.Reflection;
+ using Firebase.Database.Query;
+
+ 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);
+ }
+ }
+}
diff --git a/FireBase/Offline/ISetHandler.cs b/FireBase/Offline/ISetHandler.cs
new file mode 100644
index 0000000..477c36b
--- /dev/null
+++ b/FireBase/Offline/ISetHandler.cs
@@ -0,0 +1,11 @@
+namespace Firebase.Database.Offline
+{
+ using Firebase.Database.Query;
+
+ using System.Threading.Tasks;
+
+ public interface ISetHandler<in T>
+ {
+ Task SetAsync(ChildQuery query, string key, OfflineEntry entry);
+ }
+}
diff --git a/FireBase/Offline/InitialPullStrategy.cs b/FireBase/Offline/InitialPullStrategy.cs
new file mode 100644
index 0000000..70f6b8c
--- /dev/null
+++ b/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,
+ }
+}
diff --git a/FireBase/Offline/Internals/MemberAccessVisitor.cs b/FireBase/Offline/Internals/MemberAccessVisitor.cs
new file mode 100644
index 0000000..1f7cb11
--- /dev/null
+++ b/FireBase/Offline/Internals/MemberAccessVisitor.cs
@@ -0,0 +1,51 @@
+namespace Firebase.Database.Offline.Internals
+{
+ using System.Collections.Generic;
+ using System.Linq.Expressions;
+ using System.Reflection;
+
+ using Newtonsoft.Json;
+
+ public class MemberAccessVisitor : ExpressionVisitor
+ {
+ private readonly IList<string> propertyNames = new List<string>();
+
+ private bool wasDictionaryAccess;
+
+ public IEnumerable<string> PropertyNames => this.propertyNames;
+
+ public MemberAccessVisitor()
+ {
+ }
+
+ public override Expression Visit(Expression expr)
+ {
+ if (expr?.NodeType == ExpressionType.MemberAccess)
+ {
+ if (this.wasDictionaryAccess)
+ {
+ this.wasDictionaryAccess = false;
+ }
+ else
+ {
+ var memberExpr = (MemberExpression)expr;
+ var jsonAttr = memberExpr.Member.GetCustomAttribute<JsonPropertyAttribute>();
+
+ this.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();
+ this.propertyNames.Add(e.DynamicInvoke().ToString());
+ this.wasDictionaryAccess = callExpr.Arguments[0].NodeType == ExpressionType.MemberAccess;
+ }
+ }
+
+ return base.Visit(expr);
+ }
+ }
+}
diff --git a/FireBase/Offline/OfflineCacheAdapter.cs b/FireBase/Offline/OfflineCacheAdapter.cs
new file mode 100644
index 0000000..a3761a0
--- /dev/null
+++ b/FireBase/Offline/OfflineCacheAdapter.cs
@@ -0,0 +1,165 @@
+namespace Firebase.Database.Offline
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ 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 int Count => this.database.Count;
+
+ public bool IsSynchronized { get; }
+
+ public object SyncRoot { get; }
+
+ public bool IsReadOnly => this.database.IsReadOnly;
+
+ object IDictionary.this[object key]
+ {
+ get
+ {
+ return this.database[key.ToString()].Deserialize<T>();
+ }
+
+ set
+ {
+ var keyString = key.ToString();
+ if (this.database.ContainsKey(keyString))
+ {
+ this.database[keyString] = new OfflineEntry(keyString, value, this.database[keyString].Priority, this.database[keyString].SyncOptions);
+ }
+ else
+ {
+ this.database[keyString] = new OfflineEntry(keyString, value, 1, SyncOptions.None);
+ }
+ }
+ }
+
+ public ICollection<string> Keys => this.database.Keys;
+
+ ICollection IDictionary.Values { get; }
+
+ ICollection IDictionary.Keys { get; }
+
+ public ICollection<T> Values => this.database.Values.Select(o => o.Deserialize<T>()).ToList();
+
+ public T this[string key]
+ {
+ get
+ {
+ return this.database[key].Deserialize<T>();
+ }
+
+ set
+ {
+ if (this.database.ContainsKey(key))
+ {
+ this.database[key] = new OfflineEntry(key, value, this.database[key].Priority, this.database[key].SyncOptions);
+ }
+ else
+ {
+ this.database[key] = new OfflineEntry(key, value, 1, SyncOptions.None);
+ }
+ }
+ }
+
+ public bool Contains(object key)
+ {
+ return this.ContainsKey(key.ToString());
+ }
+
+ IDictionaryEnumerator IDictionary.GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Remove(object key)
+ {
+ this.Remove(key.ToString());
+ }
+
+ public bool IsFixedSize => false;
+
+ public IEnumerator<KeyValuePair<string, T>> GetEnumerator()
+ {
+ return this.database.Select(d => new KeyValuePair<string, T>(d.Key, d.Value.Deserialize<T>())).GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+
+ public void Add(KeyValuePair<string, T> item)
+ {
+ this.Add(item.Key, item.Value);
+ }
+
+ public void Add(object key, object value)
+ {
+ this.Add(key.ToString(), (T)value);
+ }
+
+ public void Clear()
+ {
+ this.database.Clear();
+ }
+
+ public bool Contains(KeyValuePair<string, T> item)
+ {
+ return this.ContainsKey(item.Key);
+ }
+
+ public void CopyTo(KeyValuePair<string, T>[] array, int arrayIndex)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool Remove(KeyValuePair<string, T> item)
+ {
+ return this.database.Remove(item.Key);
+ }
+
+ public void Add(string key, T value)
+ {
+ this.database.Add(key, new OfflineEntry(key, value, 1, SyncOptions.None));
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return this.database.ContainsKey(key);
+ }
+
+ public bool Remove(string key)
+ {
+ return this.database.Remove(key);
+ }
+
+ public bool TryGetValue(string key, out T value)
+ {
+ OfflineEntry val;
+
+ if (this.database.TryGetValue(key, out val))
+ {
+ value = val.Deserialize<T>();
+ return true;
+ }
+
+ value = default(T);
+ return false;
+ }
+ }
+}
diff --git a/FireBase/Offline/OfflineDatabase.cs b/FireBase/Offline/OfflineDatabase.cs
new file mode 100644
index 0000000..9cebf9c
--- /dev/null
+++ b/FireBase/Offline/OfflineDatabase.cs
@@ -0,0 +1,201 @@
+namespace Firebase.Database.Offline
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+
+ using LiteDB;
+
+ /// <summary>
+ /// The offline database.
+ /// </summary>
+ public class OfflineDatabase : IDictionary<string, OfflineEntry>
+ {
+ private readonly LiteRepository db;
+ private readonly IDictionary<string, OfflineEntry> cache;
+
+ /// <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 = this.GetFileName(itemType.ToString());
+ if(fullName.Length > 100)
+ {
+ fullName = fullName.Substring(0, 100);
+ }
+
+ BsonMapper mapper = BsonMapper.Global;
+ mapper.Entity<OfflineEntry>().Id(o => o.Key);
+
+ string root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ string filename = fullName + filenameModifier + ".db";
+ var path = Path.Combine(root, filename);
+ this.db = new LiteRepository(new LiteDatabase(path, mapper));
+
+ this.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 => this.cache.Count;
+
+ /// <summary>
+ /// Gets a value indicating whether this is a read-only collection.
+ /// </summary>
+ public bool IsReadOnly => this.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 => this.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 => this.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
+ {
+ return this.cache[key];
+ }
+
+ set
+ {
+ this.cache[key] = value;
+ this.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 this.cache.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.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)
+ {
+ this.Add(item.Key, item.Value);
+ }
+
+ /// <summary>
+ /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </summary>
+ public void Clear()
+ {
+ this.cache.Clear();
+ this.db.Delete<OfflineEntry>(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 this.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)
+ {
+ this.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 this.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 this.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)
+ {
+ this.cache.Add(key, value);
+ this.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)
+ {
+ this.cache.Remove(key);
+ return this.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 this.cache.TryGetValue(key, out value);
+ }
+
+ private string GetFileName(string fileName)
+ {
+ var invalidChars = new[] { '`', '[', ',', '=' };
+ foreach(char c in invalidChars.Concat(System.IO.Path.GetInvalidFileNameChars()).Distinct())
+ {
+ fileName = fileName.Replace(c, '_');
+ }
+
+ return fileName;
+ }
+ }
+}
diff --git a/FireBase/Offline/OfflineEntry.cs b/FireBase/Offline/OfflineEntry.cs
new file mode 100644
index 0000000..3b862cb
--- /dev/null
+++ b/FireBase/Offline/OfflineEntry.cs
@@ -0,0 +1,116 @@
+namespace Firebase.Database.Offline
+{
+ using System;
+
+ using Newtonsoft.Json;
+
+ /// <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)
+ {
+ this.Key = key;
+ this.Priority = priority;
+ this.Data = data;
+ this.Timestamp = DateTime.UtcNow;
+ this.SyncOptions = syncOptions;
+ this.IsPartial = isPartial;
+
+ this.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)(this.dataInstance ?? (this.dataInstance = JsonConvert.DeserializeObject<T>(this.Data)));
+ }
+ }
+}
diff --git a/FireBase/Offline/RealtimeDatabase.cs b/FireBase/Offline/RealtimeDatabase.cs
new file mode 100644
index 0000000..61a7010
--- /dev/null
+++ b/FireBase/Offline/RealtimeDatabase.cs
@@ -0,0 +1,459 @@
+namespace Firebase.Database.Offline
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Reactive.Linq;
+ using System.Reactive.Subjects;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using Firebase.Database.Extensions;
+ using Firebase.Database.Query;
+ using Firebase.Database.Streaming;
+ using System.Reactive.Threading.Tasks;
+ using System.Linq.Expressions;
+ using Internals;
+ using Newtonsoft.Json;
+ using System.Reflection;
+ using System.Reactive.Disposables;
+
+ /// <summary>
+ /// The real-time Database which synchronizes online and offline data.
+ /// </summary>
+ /// <typeparam name="T"> Type of entities. </typeparam>
+ public partial class RealtimeDatabase<T> : IDisposable where T : class
+ {
+ private readonly ChildQuery childQuery;
+ private readonly string elementRoot;
+ private readonly StreamingOptions streamingOptions;
+ private readonly Subject<FirebaseEvent<T>> subject;
+ private readonly InitialPullStrategy initialPullStrategy;
+ private readonly bool pushChanges;
+ private readonly FirebaseCache<T> firebaseCache;
+
+ private bool isSyncRunning;
+ private IObservable<FirebaseEvent<T>> observable;
+ private FirebaseSubscription<T> firebaseSubscription;
+
+ /// <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;
+ this.Database = offlineDatabaseFactory(typeof(T), filenameModifier);
+ this.firebaseCache = new FirebaseCache<T>(new OfflineCacheAdapter<string, T>(this.Database));
+ this.subject = new Subject<FirebaseEvent<T>>();
+
+ this.PutHandler = setHandler ?? new SetHandler<T>();
+
+ this.isSyncRunning = true;
+ Task.Factory.StartNew(this.SynchronizeThread, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ }
+
+ /// <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>
+ /// Gets the backing Database.
+ /// </summary>
+ public IDictionary<string, OfflineEntry> Database
+ {
+ get;
+ private set;
+ }
+
+ public ISetHandler<T> PutHandler
+ {
+ private get;
+ set;
+ }
+
+ /// <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)
+ {
+ this.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 = this.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 = this.firebaseCache.PushData("/" + fullKey.Item1, serializedObject).First();
+
+ if (!this.Database.ContainsKey(key) || this.Database[key].SyncOptions != SyncOptions.Patch && this.Database[key].SyncOptions != SyncOptions.Put)
+ {
+ this.Database[fullKey.Item1] = new OfflineEntry(fullKey.Item1, value, serializedObject, priority, syncOptions, true);
+ }
+
+ this.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 (!this.Database.ContainsKey(key))
+ {
+ this.Database[key] = new OfflineEntry(key, null, priority, SyncOptions.Pull);
+ }
+ else if (this.Database[key].SyncOptions == SyncOptions.None)
+ {
+ // pull only if push isn't pending
+ this.Database[key].SyncOptions = SyncOptions.Pull;
+ }
+ }
+
+ /// <summary>
+ /// Fetches everything from the remote database.
+ /// </summary>
+ public async Task PullAsync()
+ {
+ var existingEntries = await this.childQuery
+ .OnceAsync<T>()
+ .ToObservable()
+ .RetryAfterDelay<IReadOnlyCollection<FirebaseObject<T>>, FirebaseException>(
+ this.childQuery.Client.Options.SyncPeriod,
+ ex => ex.StatusCode == System.Net.HttpStatusCode.OK) // OK implies the request couldn't complete due to network error.
+ .Select(e => this.ResetDatabaseFromInitial(e, false))
+ .SelectMany(e => e)
+ .Do(e =>
+ {
+ this.Database[e.Key] = new OfflineEntry(e.Key, e.Object, 1, SyncOptions.None);
+ this.subject.OnNext(new FirebaseEvent<T>(e.Key, e.Object, FirebaseEventType.InsertOrUpdate, FirebaseEventSource.OnlinePull));
+ })
+ .ToList();
+
+ // Remove items not stored online
+ foreach (var item in this.Database.Keys.Except(existingEntries.Select(f => f.Key)).ToList())
+ {
+ this.Database.Remove(item);
+ this.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 this.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 (!this.isSyncRunning)
+ {
+ this.isSyncRunning = true;
+ Task.Factory.StartNew(this.SynchronizeThread, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ }
+
+ if (this.observable == null)
+ {
+ var initialData = Observable.Return(FirebaseEvent<T>.Empty(FirebaseEventSource.Offline));
+ if(this.Database.TryGetValue(this.elementRoot, out OfflineEntry 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(this.Database.Count > 0)
+ {
+ initialData = this.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();
+ }
+
+ this.observable = initialData
+ .Merge(this.subject)
+ .Merge(this.GetInitialPullObservable()
+ .RetryAfterDelay<IReadOnlyCollection<FirebaseObject<T>>, FirebaseException>(
+ this.childQuery.Client.Options.SyncPeriod,
+ ex => ex.StatusCode == System.Net.HttpStatusCode.OK) // OK implies the request couldn't complete due to network error.
+ .Select(e => this.ResetDatabaseFromInitial(e))
+ .SelectMany(e => e)
+ .Do(this.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 => this.InitializeStreamingSubscription(observer))))
+ .Do(next => { }, e => this.observable = null, () => this.observable = null)
+ .Replay()
+ .RefCount();
+ }
+
+ return this.observable;
+ }
+
+ public void Dispose()
+ {
+ this.subject.OnCompleted();
+ this.firebaseSubscription?.Dispose();
+ }
+
+ private IReadOnlyCollection<FirebaseObject<T>> ResetDatabaseFromInitial(IReadOnlyCollection<FirebaseObject<T>> collection, bool onlyWhenInitialEverything = true)
+ {
+ if (onlyWhenInitialEverything && this.initialPullStrategy != InitialPullStrategy.Everything)
+ {
+ return collection;
+ }
+
+ // items which are in local db, but not in the online collection
+ var extra = this.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 (!this.Database.ContainsKey(e.Key) || this.Database[e.Key].SyncOptions == SyncOptions.None || this.Database[e.Key].SyncOptions == SyncOptions.Pull || this.initialPullStrategy != InitialPullStrategy.Everything)
+ {
+ this.Database[e.Key] = new OfflineEntry(e.Key, e.Object, 1, SyncOptions.None);
+ }
+ }
+
+ private IObservable<IReadOnlyCollection<FirebaseObject<T>>> GetInitialPullObservable()
+ {
+ FirebaseQuery query;
+ switch (this.initialPullStrategy)
+ {
+ case InitialPullStrategy.MissingOnly:
+ query = this.childQuery.OrderByKey().StartAt(() => this.GetLatestKey());
+ break;
+ case InitialPullStrategy.Everything:
+ query = this.childQuery;
+ break;
+ case InitialPullStrategy.None:
+ default:
+ return Observable.Empty<IReadOnlyCollection<FirebaseEvent<T>>>();
+ }
+
+ if (string.IsNullOrWhiteSpace(this.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>(this.elementRoot, e) }));
+ }
+
+ private IDisposable InitializeStreamingSubscription(IObserver<FirebaseEvent<T>> observer)
+ {
+ var completeDisposable = Disposable.Create(() => this.isSyncRunning = false);
+
+ switch (this.streamingOptions)
+ {
+ case StreamingOptions.LatestOnly:
+ // stream since the latest key
+ var queryLatest = this.childQuery.OrderByKey().StartAt(() => this.GetLatestKey());
+ this.firebaseSubscription = new FirebaseSubscription<T>(observer, queryLatest, this.elementRoot, this.firebaseCache);
+ this.firebaseSubscription.ExceptionThrown += this.StreamingExceptionThrown;
+
+ return new CompositeDisposable(this.firebaseSubscription.Run(), completeDisposable);
+ case StreamingOptions.Everything:
+ // stream everything
+ var queryAll = this.childQuery;
+ this.firebaseSubscription = new FirebaseSubscription<T>(observer, queryAll, this.elementRoot, this.firebaseCache);
+ this.firebaseSubscription.ExceptionThrown += this.StreamingExceptionThrown;
+
+ return new CompositeDisposable(this.firebaseSubscription.Run(), completeDisposable);
+ default:
+ break;
+ }
+
+ return completeDisposable;
+ }
+
+ private void SetAndRaise(string key, OfflineEntry obj, FirebaseEventSource eventSource = FirebaseEventSource.Offline)
+ {
+ this.Database[key] = obj;
+ this.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 (this.isSyncRunning)
+ {
+ try
+ {
+ var validEntries = this.Database.Where(e => e.Value != null);
+ await this.PullEntriesAsync(validEntries.Where(kvp => kvp.Value.SyncOptions == SyncOptions.Pull));
+
+ if (this.pushChanges)
+ {
+ await this.PushEntriesAsync(validEntries.Where(kvp => kvp.Value.SyncOptions == SyncOptions.Put || kvp.Value.SyncOptions == SyncOptions.Patch));
+ }
+ }
+ catch (Exception ex)
+ {
+ this.SyncExceptionThrown?.Invoke(this, new ExceptionEventArgs(ex));
+ }
+
+ await Task.Delay(this.childQuery.Client.Options.SyncPeriod);
+ }
+ }
+
+ private string GetLatestKey()
+ {
+ var key = this.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 ?
+ this.ResetSyncAfterPush(this.PutHandler.SetAsync(this.childQuery, kvp.Key, kvp.Value), kvp.Key) :
+ this.ResetSyncAfterPush(this.PutHandler.SetAsync(this.childQuery, kvp.Key, kvp.Value), kvp.Key, kvp.Value.Deserialize<T>()));
+
+ try
+ {
+ await Task.WhenAll(tasks).WithAggregateException();
+ }
+ catch (Exception ex)
+ {
+ this.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 => this.ResetAfterPull(this.childQuery.Child(pair.Key == this.elementRoot ? string.Empty : pair.Key).OnceSingleAsync<T>(), pair.Key, pair.Value));
+
+ try
+ {
+ await Task.WhenAll(tasks).WithAggregateException();
+ }
+ catch (Exception ex)
+ {
+ this.SyncExceptionThrown?.Invoke(this, new ExceptionEventArgs(ex));
+ }
+ }
+ }
+
+ private async Task ResetAfterPull(Task<T> task, string key, OfflineEntry entry)
+ {
+ await task;
+ this.SetAndRaise(key, new OfflineEntry(key, task.Result, entry.Priority, SyncOptions.None), FirebaseEventSource.OnlinePull);
+ }
+
+ private async Task ResetSyncAfterPush(Task task, string key, T obj)
+ {
+ await this.ResetSyncAfterPush(task, key);
+
+ if (this.streamingOptions == StreamingOptions.None)
+ {
+ this.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;
+ this.ResetSyncOptions(key);
+ }
+
+ private void ResetSyncOptions(string key)
+ {
+ var item = this.Database[key];
+
+ if (item.IsPartial)
+ {
+ this.Database.Remove(key);
+ }
+ else
+ {
+ item.SyncOptions = SyncOptions.None;
+ this.Database[key] = item;
+ }
+ }
+
+ private void StreamingExceptionThrown(object sender, ExceptionEventArgs<FirebaseException> e)
+ {
+ this.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);
+ }
+
+ }
+}
diff --git a/FireBase/Offline/SetHandler.cs b/FireBase/Offline/SetHandler.cs
new file mode 100644
index 0000000..1efa7b6
--- /dev/null
+++ b/FireBase/Offline/SetHandler.cs
@@ -0,0 +1,24 @@
+namespace Firebase.Database.Offline
+{
+ using Firebase.Database.Query;
+
+ using System.Threading.Tasks;
+
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/FireBase/Offline/StreamingOptions.cs b/FireBase/Offline/StreamingOptions.cs
new file mode 100644
index 0000000..9ed4e54
--- /dev/null
+++ b/FireBase/Offline/StreamingOptions.cs
@@ -0,0 +1,21 @@
+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
+ }
+}
diff --git a/FireBase/Offline/SyncOptions.cs b/FireBase/Offline/SyncOptions.cs
new file mode 100644
index 0000000..b2f382a
--- /dev/null
+++ b/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
+ }
+}
diff --git a/FireBase/Query/AuthQuery.cs b/FireBase/Query/AuthQuery.cs
new file mode 100644
index 0000000..8a8d3e8
--- /dev/null
+++ b/FireBase/Query/AuthQuery.cs
@@ -0,0 +1,33 @@
+namespace Firebase.Database.Query
+{
+ using System;
+
+ /// <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 this.tokenFactory();
+ }
+ }
+}
diff --git a/FireBase/Query/ChildQuery.cs b/FireBase/Query/ChildQuery.cs
new file mode 100644
index 0000000..1696ea8
--- /dev/null
+++ b/FireBase/Query/ChildQuery.cs
@@ -0,0 +1,56 @@
+namespace Firebase.Database.Query
+{
+ using System;
+
+ /// <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 = this.pathFactory();
+
+ if (s != string.Empty && !s.EndsWith("/"))
+ {
+ s += '/';
+ }
+
+ if (!(child is ChildQuery))
+ {
+ return s + ".json";
+ }
+
+ return s;
+ }
+ }
+}
diff --git a/FireBase/Query/FilterQuery.cs b/FireBase/Query/FilterQuery.cs
new file mode 100644
index 0000000..f9f6271
--- /dev/null
+++ b/FireBase/Query/FilterQuery.cs
@@ -0,0 +1,81 @@
+namespace Firebase.Database.Query
+{
+ using System;
+ using System.Globalization;
+
+ /// <summary>
+ /// Represents a firebase filtering query, e.g. "?LimitToLast=10".
+ /// </summary>
+ public class FilterQuery : ParameterQuery
+ {
+ private readonly Func<string> valueFactory;
+ private readonly Func<double> doubleValueFactory;
+ private readonly Func<bool> boolValueFactory;
+
+ /// <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)
+ {
+ this.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)
+ {
+ this.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 (this.valueFactory != null)
+ {
+ if(this.valueFactory() == null)
+ {
+ return $"null";
+ }
+ return $"\"{this.valueFactory()}\"";
+ }
+ else if (this.doubleValueFactory != null)
+ {
+ return this.doubleValueFactory().ToString(CultureInfo.InvariantCulture);
+ }
+ else if (this.boolValueFactory != null)
+ {
+ return $"{this.boolValueFactory().ToString().ToLower()}";
+ }
+
+ return string.Empty;
+ }
+ }
+}
diff --git a/FireBase/Query/FirebaseQuery.cs b/FireBase/Query/FirebaseQuery.cs
new file mode 100644
index 0000000..0e1b84a
--- /dev/null
+++ b/FireBase/Query/FirebaseQuery.cs
@@ -0,0 +1,314 @@
+namespace Firebase.Database.Query
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Net.Http;
+ using System.Reactive.Linq;
+ using System.Threading.Tasks;
+
+ using Firebase.Database.Http;
+ using Firebase.Database.Offline;
+ using Firebase.Database.Streaming;
+
+ using Newtonsoft.Json;
+ using System.Net;
+
+ /// <summary>
+ /// Represents a firebase query.
+ /// </summary>
+ public abstract class FirebaseQuery : IFirebaseQuery, IDisposable
+ {
+ protected TimeSpan DEFAULT_HTTP_CLIENT_TIMEOUT = new TimeSpan(0, 0, 180);
+
+ protected readonly FirebaseQuery Parent;
+
+ private HttpClient client;
+
+ /// <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)
+ {
+ this.Client = client;
+ this.Parent = parent;
+ }
+
+ /// <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 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<T>(url, Client.Options.JsonSerializerSettings)
+ .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 this.BuildUrlAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ throw new FirebaseException("Couldn't build the url", string.Empty, responseData, statusCode, ex);
+ }
+
+ try
+ {
+ var response = await this.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>
+ /// 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 (this.Client.Options.AuthTokenAsyncFactory != null)
+ {
+ var token = await this.Client.Options.AuthTokenAsyncFactory().ConfigureAwait(false);
+ return this.WithAuth(token).BuildUrl(null);
+ }
+
+ return this.BuildUrl(null);
+ }
+
+ /// <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, this.Client).PutAsync(data).ConfigureAwait(false);
+
+ return new FirebaseObject<string>(key, data);
+ }
+ else
+ {
+ var c = this.GetClient(timeout);
+ var sendData = await this.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 = this.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 = this.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 = this.GetClient(timeout);
+ var url = string.Empty;
+ var responseData = string.Empty;
+ var statusCode = HttpStatusCode.OK;
+
+ try
+ {
+ url = await this.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>
+ /// Disposes this instance.
+ /// </summary>
+ public void Dispose()
+ {
+ this.client?.Dispose();
+ }
+
+ /// <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 = this.BuildUrlSegment(child);
+
+ if (this.Parent != null)
+ {
+ url = this.Parent.BuildUrl(this) + url;
+ }
+
+ return url;
+ }
+
+ private HttpClient GetClient(TimeSpan? timeout = null)
+ {
+ if (this.client == null)
+ {
+ this.client = new HttpClient();
+ }
+
+ if (!timeout.HasValue)
+ {
+ this.client.Timeout = DEFAULT_HTTP_CLIENT_TIMEOUT;
+ }
+ else
+ {
+ this.client.Timeout = timeout.Value;
+ }
+
+ return this.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 this.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);
+ }
+ }
+ }
+}
diff --git a/FireBase/Query/IFirebaseQuery.cs b/FireBase/Query/IFirebaseQuery.cs
new file mode 100644
index 0000000..2e8c671
--- /dev/null
+++ b/FireBase/Query/IFirebaseQuery.cs
@@ -0,0 +1,43 @@
+namespace Firebase.Database.Query
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+
+ using Firebase.Database.Streaming;
+
+ /// <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();
+ }
+}
diff --git a/FireBase/Query/OrderQuery.cs b/FireBase/Query/OrderQuery.cs
new file mode 100644
index 0000000..46ebd2c
--- /dev/null
+++ b/FireBase/Query/OrderQuery.cs
@@ -0,0 +1,34 @@
+namespace Firebase.Database.Query
+{
+ using System;
+
+ /// <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 $"\"{this.propertyNameFactory()}\"";
+ }
+ }
+}
diff --git a/FireBase/Query/ParameterQuery.cs b/FireBase/Query/ParameterQuery.cs
new file mode 100644
index 0000000..e3d9717
--- /dev/null
+++ b/FireBase/Query/ParameterQuery.cs
@@ -0,0 +1,43 @@
+namespace Firebase.Database.Query
+{
+ using System;
+
+ /// <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;
+ this.separator = (this.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 $"{this.separator}{this.parameterFactory()}={this.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);
+ }
+}
diff --git a/FireBase/Query/QueryExtensions.cs b/FireBase/Query/QueryExtensions.cs
new file mode 100644
index 0000000..77db644
--- /dev/null
+++ b/FireBase/Query/QueryExtensions.cs
@@ -0,0 +1,207 @@
+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);
+ }
+ }
+}
diff --git a/FireBase/Query/QueryFactoryExtensions.cs b/FireBase/Query/QueryFactoryExtensions.cs
new file mode 100644
index 0000000..b36e74a
--- /dev/null
+++ b/FireBase/Query/QueryFactoryExtensions.cs
@@ -0,0 +1,176 @@
+namespace Firebase.Database.Query
+{
+ using System;
+
+ /// <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);
+ }
+ }
+}
diff --git a/FireBase/Query/SilentQuery.cs b/FireBase/Query/SilentQuery.cs
new file mode 100644
index 0000000..15584f6
--- /dev/null
+++ b/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";
+ }
+ }
+}
diff --git a/FireBase/Settings.StyleCop b/FireBase/Settings.StyleCop
new file mode 100644
index 0000000..833aa39
--- /dev/null
+++ b/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/FireBase/Streaming/FirebaseCache.cs b/FireBase/Streaming/FirebaseCache.cs
new file mode 100644
index 0000000..ba7990b
--- /dev/null
+++ b/FireBase/Streaming/FirebaseCache.cs
@@ -0,0 +1,192 @@
+namespace Firebase.Database.Streaming
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Reflection;
+
+ using Firebase.Database.Http;
+
+ using Newtonsoft.Json;
+
+ /// <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)
+ {
+ this.dictionary = existingItems;
+ this.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] = this.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 = this.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 = this.dictionary[key];
+
+ objDeleter();
+
+ yield return new FirebaseObject<T>(key, target);
+ yield break;
+ }
+
+ // now insert the data
+ if (obj is IDictionary && !this.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.ToString() : 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, this.serializerSettings);
+ }
+
+ this.dictionary[pathElements[0]] = this.dictionary[pathElements[0]];
+ yield return new FirebaseObject<T>(pathElements[0], this.dictionary[pathElements[0]]);
+ }
+ }
+
+ public bool Contains(string key)
+ {
+ return this.dictionary.Keys.Contains(key);
+ }
+
+ private object CreateInstance(Type type)
+ {
+ if (type == typeof(string))
+ {
+ return string.Empty;
+ }
+ else
+ {
+ return Activator.CreateInstance(type);
+ }
+ }
+
+ #region IEnumerable
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+
+ public IEnumerator<FirebaseObject<T>> GetEnumerator()
+ {
+ return this.dictionary.Select(p => new FirebaseObject<T>(p.Key, p.Value)).GetEnumerator();
+ }
+
+ #endregion
+ }
+}
diff --git a/FireBase/Streaming/FirebaseEvent.cs b/FireBase/Streaming/FirebaseEvent.cs
new file mode 100644
index 0000000..c2338ca
--- /dev/null
+++ b/FireBase/Streaming/FirebaseEvent.cs
@@ -0,0 +1,40 @@
+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)
+ {
+ this.EventType = eventType;
+ this.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) => new FirebaseEvent<T>(string.Empty, default(T), FirebaseEventType.InsertOrUpdate, source);
+ }
+}
diff --git a/FireBase/Streaming/FirebaseEventSource.cs b/FireBase/Streaming/FirebaseEventSource.cs
new file mode 100644
index 0000000..98df977
--- /dev/null
+++ b/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
+ }
+}
diff --git a/FireBase/Streaming/FirebaseEventType.cs b/FireBase/Streaming/FirebaseEventType.cs
new file mode 100644
index 0000000..5fb21ef
--- /dev/null
+++ b/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
+ }
+}
diff --git a/FireBase/Streaming/FirebaseServerEventType.cs b/FireBase/Streaming/FirebaseServerEventType.cs
new file mode 100644
index 0000000..1f10bc8
--- /dev/null
+++ b/FireBase/Streaming/FirebaseServerEventType.cs
@@ -0,0 +1,15 @@
+namespace Firebase.Database.Streaming
+{
+ internal enum FirebaseServerEventType
+ {
+ Put,
+
+ Patch,
+
+ KeepAlive,
+
+ Cancel,
+
+ AuthRevoked
+ }
+}
diff --git a/FireBase/Streaming/FirebaseSubscription.cs b/FireBase/Streaming/FirebaseSubscription.cs
new file mode 100644
index 0000000..4b5e643
--- /dev/null
+++ b/FireBase/Streaming/FirebaseSubscription.cs
@@ -0,0 +1,221 @@
+namespace Firebase.Database.Streaming
+{
+ using System;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Net.Http;
+ using System.Net.Http.Headers;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ using Firebase.Database.Query;
+
+ using Newtonsoft.Json.Linq;
+ using System.Net;
+
+ /// <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 readonly CancellationTokenSource cancel;
+ private readonly IObserver<FirebaseEvent<T>> observer;
+ private readonly IFirebaseQuery query;
+ private readonly FirebaseCache<T> cache;
+ private readonly string elementRoot;
+ private readonly FirebaseClient client;
+
+ private static HttpClient http;
+
+ 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;
+ this.cancel = new CancellationTokenSource();
+ this.cache = cache;
+ this.client = query.Client;
+ }
+
+ public event EventHandler<ExceptionEventArgs<FirebaseException>> ExceptionThrown;
+
+ public void Dispose()
+ {
+ this.cancel.Cancel();
+ }
+
+ public IDisposable Run()
+ {
+ Task.Run(() => this.ReceiveThread());
+
+ return this;
+ }
+
+ private async void ReceiveThread()
+ {
+ while (true)
+ {
+ var url = string.Empty;
+ var line = string.Empty;
+ var statusCode = HttpStatusCode.OK;
+
+ try
+ {
+ this.cancel.Token.ThrowIfCancellationRequested();
+
+ // initialize network connection
+ url = await this.query.BuildUrlAsync().ConfigureAwait(false);
+ var request = new HttpRequestMessage(HttpMethod.Get, url);
+ var serverEvent = FirebaseServerEventType.KeepAlive;
+
+ var client = this.GetHttpClient();
+ var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, this.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)
+ {
+ this.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 = this.ParseServerEvent(serverEvent, tuple[1]);
+ break;
+ case "data":
+ this.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)
+ {
+ this.observer.OnError(new FirebaseException(url, string.Empty, line, statusCode, ex));
+ this.Dispose();
+ break;
+ }
+ catch (Exception ex)
+ {
+ this.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(this.elementRoot) || !this.cache.Contains(this.elementRoot))
+ {
+ if(path == "/" && data == string.Empty)
+ {
+ this.observer.OnNext(FirebaseEvent<T>.Empty(FirebaseEventSource.OnlineStream));
+ return;
+ }
+ }
+
+ var eventType = string.IsNullOrWhiteSpace(data) ? FirebaseEventType.Delete : FirebaseEventType.InsertOrUpdate;
+
+ var items = this.cache.PushData(this.elementRoot + path, data);
+
+ foreach (var i in items.ToList())
+ {
+ this.observer.OnNext(new FirebaseEvent<T>(i.Key, i.Object, eventType, FirebaseEventSource.OnlineStream));
+ }
+
+ break;
+ case FirebaseServerEventType.KeepAlive:
+ break;
+ case FirebaseServerEventType.Cancel:
+ this.observer.OnError(new FirebaseException(url, string.Empty, serverData, HttpStatusCode.Unauthorized));
+ this.Dispose();
+ break;
+ }
+ }
+
+ private HttpClient GetHttpClient()
+ {
+ return http;
+ }
+ }
+}
diff --git a/FireBase/Streaming/NonBlockingStreamReader.cs b/FireBase/Streaming/NonBlockingStreamReader.cs
new file mode 100644
index 0000000..2ac83fd
--- /dev/null
+++ b/FireBase/Streaming/NonBlockingStreamReader.cs
@@ -0,0 +1,60 @@
+namespace Firebase.Database.Streaming
+{
+ using System.IO;
+ using System.Text;
+
+ /// <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 Stream stream;
+ private readonly byte[] buffer;
+ private readonly int bufferSize;
+
+ private string cachedData;
+
+ public NonBlockingStreamReader(Stream stream, int bufferSize = DefaultBufferSize)
+ {
+ this.stream = stream;
+ this.bufferSize = bufferSize;
+ this.buffer = new byte[bufferSize];
+
+ this.cachedData = string.Empty;
+ }
+
+ public override string ReadLine()
+ {
+ var currentString = this.TryGetNewLine();
+
+ while (currentString == null)
+ {
+ var read = this.stream.Read(this.buffer, 0, this.bufferSize);
+ var str = Encoding.UTF8.GetString(buffer, 0, read);
+
+ cachedData += str;
+ currentString = this.TryGetNewLine();
+ }
+
+ return currentString;
+ }
+
+ private string TryGetNewLine()
+ {
+ var newLine = cachedData.IndexOf('\n');
+
+ if (newLine >= 0)
+ {
+ var r = cachedData.Substring(0, newLine + 1);
+ this.cachedData = cachedData.Remove(0, r.Length);
+ return r.Trim();
+ }
+
+ return null;
+ }
+ }
+}