summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--DSACore/Commands/CommandHandler.cs3
-rw-r--r--DSACore/DSACore.csproj2
-rw-r--r--DSACore/DSA_Game/Dsa.cs4
-rw-r--r--DSACore/DSA_Game/Save/Session.cs2
-rw-r--r--DSACore/FireBase/Database.cs24
-rw-r--r--DSACore/Hubs/ChatHub.cs40
-rw-r--r--DSACore/Models/User.cs13
-rw-r--r--DSACore/Properties/PublishProfiles/FolderProfile1.pubxml2
-rw-r--r--DSACore/Properties/launchSettings.json2
-rw-r--r--DSACore/PropertiesDSACore-Audio-Sound.json7
-rw-r--r--DSACore/PropertiesDSACore-Auxiliary-CommandInfo.json101
-rw-r--r--DSACore/PropertiesDSACore-DSA_Game-Characters-Character.json290
-rw-r--r--DSACore/PropertiesNewtonsoft-Json-Linq-JProperty.json30
-rw-r--r--DSACore/Startup.cs21
-rw-r--r--DiscoBot.sln39
-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
-rw-r--r--WebInterface/NodeJSServer/NodeJSServer.njsproj84
-rw-r--r--WebInterface/NodeJSServer/dist/Web.config15
-rw-r--r--WebInterface/NodeJSServer/dist/chat.js10
-rw-r--r--WebInterface/Web.config15
-rw-r--r--ZooBOTanica/CritCreate.cs2
62 files changed, 4429 insertions, 18 deletions
diff --git a/DSACore/Commands/CommandHandler.cs b/DSACore/Commands/CommandHandler.cs
index 446f99e..4b6ca42 100644
--- a/DSACore/Commands/CommandHandler.cs
+++ b/DSACore/Commands/CommandHandler.cs
@@ -97,7 +97,6 @@ namespace DSACore.Commands
private static string CheckCommand(string name, CommandTypes command, string waffe, int erschwernis = 0)
{
var chr = Dsa.GetCharacter(0);
- throw new NotImplementedException("access char by id ore name and group id");
switch (command)
{
@@ -116,6 +115,8 @@ namespace DSACore.Commands
}
return $"{name} verwendet {waffe}";
+
+ throw new NotImplementedException("access char by id ore name and group id");
}
}
}
diff --git a/DSACore/DSACore.csproj b/DSACore/DSACore.csproj
index 67bd4ab..ad760c2 100644
--- a/DSACore/DSACore.csproj
+++ b/DSACore/DSACore.csproj
@@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
+ <StartupObject>DSACore.Program</StartupObject>
</PropertyGroup>
<ItemGroup>
@@ -14,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\DSALib\DSALib.csproj" />
+ <ProjectReference Include="..\FireBase\FireBase.csproj" />
</ItemGroup>
</Project>
diff --git a/DSACore/DSA_Game/Dsa.cs b/DSACore/DSA_Game/Dsa.cs
index 7258e8c..94de194 100644
--- a/DSACore/DSA_Game/Dsa.cs
+++ b/DSACore/DSA_Game/Dsa.cs
@@ -13,7 +13,7 @@ namespace DSACore.DSA_Game
public static class Dsa
{
- public const string rootPath = "DiscoBot\\DSACore\\";
+ public const string rootPath = "";//"DiscoBot\\DSACore\\";
private static Session s_session;
@@ -46,7 +46,7 @@ namespace DSACore.DSA_Game
//new .Auxiliary.Calculator.StringSolver("1d100 - (1d200 + 1) * -50000").Solve();
/*Session = new Session();*/
// relation.Add("Papo", "Pump aus der Gosse");
- foreach (var filename in Directory.GetFiles("helden", "*.xml"))
+ foreach (var filename in Directory.GetFiles(rootPath + "helden", "*.xml"))
{
Chars.Add(new Character(filename));
(Chars.Last() as Character)?.Talente.Select(x => new Talent(x.Name, x.Probe, 0))
diff --git a/DSACore/DSA_Game/Save/Session.cs b/DSACore/DSA_Game/Save/Session.cs
index d343920..b402656 100644
--- a/DSACore/DSA_Game/Save/Session.cs
+++ b/DSACore/DSA_Game/Save/Session.cs
@@ -9,7 +9,7 @@ namespace DSACore.DSA_Game.Save
public class Session
{
- public static string DirectoryPath { get; set; } = @"sessions";
+ public static string DirectoryPath { get; set; } = Dsa.rootPath + @"sessions";
public Dictionary<string, string> Relation { get; set; } = new Dictionary<string, string>(); // dictionary to match the char
diff --git a/DSACore/FireBase/Database.cs b/DSACore/FireBase/Database.cs
new file mode 100644
index 0000000..cd21c00
--- /dev/null
+++ b/DSACore/FireBase/Database.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Firebase.Database;
+using Firebase.Database.Query;
+
+
+namespace DSACore.FireBase
+{
+ public static class Database
+ {
+ static Database()
+ {
+ var auth = "ABCDE"; // your app secret
+ var firebaseClient = new FirebaseClient(
+ "<URL>",
+ new FirebaseOptions
+ {
+ AuthTokenAsyncFactory = () => Task.FromResult(auth)
+ });
+ }
+ }
+}
diff --git a/DSACore/Hubs/ChatHub.cs b/DSACore/Hubs/ChatHub.cs
index 6af8c38..a8bf883 100644
--- a/DSACore/Hubs/ChatHub.cs
+++ b/DSACore/Hubs/ChatHub.cs
@@ -2,15 +2,53 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using DSACore.Models;
using Microsoft.AspNetCore.SignalR;
namespace DSACore.Hubs
{
public class ChatHub : Hub
{
+ private static Dictionary<string, User> UserGroup;
+
public async Task SendMessage(string user, string message)
{
- await Clients.All.SendAsync("ReciveMessage", user, message);
+ var args = message.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList();
+ var ident = args.First().Replace("!", "");
+ if(args.Count>0){args.RemoveAt(0);}
+
+ await SendToGroup(UserGroup[Context.ConnectionId].Group, user, Commands.CommandHandler.ExecuteCommand(new Command{CharId = 0,CmdIdentifier = ident, CmdTexts = args, Name = user}));
+ }
+
+ private Task SendToGroup(string group, string user, string message)
+ {
+ return Clients.Group(group).SendCoreAsync("ReceiveMessage", new object[] { user, message });
+ }
+
+ public async Task GetGroups()
+ {
+ await Clients.Caller.SendCoreAsync("ListGroups", new object[] { "TheCrew", "Testdata" });
+ throw new NotImplementedException("add database call to get groups");
+ }
+
+ public async Task Login(string group, string password)
+ {
+ if (password == "valid")
+ {
+ UserGroup.Add(Context.ConnectionId, new User{Group = group});
+ await Groups.AddToGroupAsync(Context.ConnectionId, group);
+ }
+
+ await SendToGroup(group, "", "Ein neuer Nutzer hat die Gruppe betreten");
+ }
+
+ public async Task Disconnect()
+ {
+ var user = UserGroup[Context.ConnectionId];
+ await Groups.RemoveFromGroupAsync(Context.ConnectionId, user.Group);
+
+ await SendToGroup(user.Group, user.Name, "Connection lost");
}
+
}
}
diff --git a/DSACore/Models/User.cs b/DSACore/Models/User.cs
new file mode 100644
index 0000000..105e564
--- /dev/null
+++ b/DSACore/Models/User.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace DSACore.Models
+{
+ public class User
+ {
+ public string Name { get; set; }
+ public string Group { get; set; }
+ }
+}
diff --git a/DSACore/Properties/PublishProfiles/FolderProfile1.pubxml b/DSACore/Properties/PublishProfiles/FolderProfile1.pubxml
index e929098..e03b55a 100644
--- a/DSACore/Properties/PublishProfiles/FolderProfile1.pubxml
+++ b/DSACore/Properties/PublishProfiles/FolderProfile1.pubxml
@@ -13,7 +13,7 @@ indem Sie diese MSBuild-Datei bearbeiten. Weitere Informationen hierzu finden Si
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
<ExcludeApp_Data>False</ExcludeApp_Data>
<TargetFramework>netcoreapp2.1</TargetFramework>
- <RuntimeIdentifier>win-x64</RuntimeIdentifier>
+ <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<ProjectGuid>35a5e2cc-0fd4-4bc0-acbf-38599caed1c4</ProjectGuid>
<SelfContained>false</SelfContained>
<_IsPortable>true</_IsPortable>
diff --git a/DSACore/Properties/launchSettings.json b/DSACore/Properties/launchSettings.json
index 2da5fec..50d10a9 100644
--- a/DSACore/Properties/launchSettings.json
+++ b/DSACore/Properties/launchSettings.json
@@ -21,7 +21,7 @@
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/commands",
- "applicationUrl": "https://0.0.0.0:5001;http://0.0.0.0:5000",
+ "applicationUrl": "https://192.168.2.58:5001;http://192.168.2.58:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/DSACore/PropertiesDSACore-Audio-Sound.json b/DSACore/PropertiesDSACore-Audio-Sound.json
new file mode 100644
index 0000000..87a0e6b
--- /dev/null
+++ b/DSACore/PropertiesDSACore-Audio-Sound.json
@@ -0,0 +1,7 @@
+[
+ {
+ "Name": "Test",
+ "Url": "http",
+ "Volume": 100
+ }
+] \ No newline at end of file
diff --git a/DSACore/PropertiesDSACore-Auxiliary-CommandInfo.json b/DSACore/PropertiesDSACore-Auxiliary-CommandInfo.json
new file mode 100644
index 0000000..b9941f2
--- /dev/null
+++ b/DSACore/PropertiesDSACore-Auxiliary-CommandInfo.json
@@ -0,0 +1,101 @@
+[
+ {
+ "Name": "ich bin",
+ "Scope": "All",
+ "Brief": "Setzt den gespielten Charakter fest",
+ "Description": [
+ "Mit \"!Ich bin\" kann der gespielte Charakter definiert, bzw. gewechselt werden.\n",
+ " Die Charaktere müssen als *.xml Dateien hinterlegt sein.\n\n",
+ " !ich Zeigt an welcher Charakter zur Zeit gespielt wird\n",
+ " !ich bin Zalibius Wechsel zum Helden Zalibius\n",
+ " !ich Rhoktar Orkische Variante von !ich bin.\n",
+ " Wechselt zu Rhoktar.\n\n",
+ " !list chars Zeigt die Liste verfügbarer Charaktere.\n",
+ " \n"
+ ]
+ },
+ {
+ "Name": "List",
+ "Scope": "All",
+ "Brief": "Anzeige vonSpielrelevanten Listen",
+ "Description": [
+ "Mit \"!list\" lassen sich spielrelevante Listen ausgeben. Die Angezeigte Liste wird nach einiger Zeit wieder gelöscht.\n",
+ "\n",
+ " !list chars Liste aller verfügbaren Helden (eingelesen per *.xml)\n",
+ " und NSCs.\n",
+ " (Mit \"!ich bin\" kann der Held ausgewählt werden.)\n",
+ " !list commands Liste aller verwendbaren Bot-Kommandos.\n",
+ " !list sounds Liste der Soundeffekte."
+ ]
+ },
+ {
+ "Name": "Held",
+ "Scope": "All",
+ "Brief": "Anzeige von Heldenwerten",
+ "Description": [
+ "Mit \"!Held\" lassen sich Heldenwerte ausgeben. Mehrere Werte können gleichzeitig angefordert werde. \"!Held LE Waffen\" liefert so z.B. Informationen zur Lebensenergie und den Kampfwerten.\n Bis auf wenige Ausnahmen wird die Angezeigte Liste nach einiger Zeit wieder gelöscht.\n",
+ "\n",
+ " !Held Zeigt den Heldenbrief an.\n",
+ " (Diese Liste wird nicht automatisch gelöscht)\n",
+ " !Held Eigenschaften Zeigt die Eigenschaften MU/KL/CH/IN/KK/GE/FF/KO.\n",
+ " !Held e Kurzform von \"!Held Eigenschaften\".\n",
+ " !Held LE Zeigt LE an.\n",
+ " !Held AE Zeigt AE an.\n",
+ " !Held stats Zeigt Eigenschaften und zusätzlich SO/LE/AE.\n",
+ " !Held Kampfwerte Zeigt AT/PA für aktivierte Waffentalente.\n",
+ " !Held Waffe/!list w Kurzformen von \"!Held Kampfwerte\".\n",
+ " !Held Vorteile Zeigt Vor- und Nachteile an.\n",
+ " !Held v Kurzform von \"!Held Vorteile\".\n",
+ " !Held Talente Zeigt die Liste aller aktivierten Talente, deren TaW,\n",
+ " sowie die Probe.\n",
+ " !Held t Kurzform von \"!Held Talente\".\n",
+ " !Held Zauber Zeigt die Liste aller aktivierten Zauber, deren ZaW,\n",
+ " sowie die Probe.\n",
+ " !Held z Kurzform von \"!Held Zauber\".\n"
+ ]
+ },
+ {
+ "Name": "LE",
+ "Scope": "All",
+ "Brief": "Ändert dein Leben - im wahrsten Sinne des Wortes",
+ "Description": [
+ "Mit !LE zeigt man die Lebensenergie an, ändert sie, oder setzt sie auf einen neuen Wert\n\n",
+ " !LE Zeigt Lebensenergie an\n",
+ " !LE 30 Setzt LE auf 30\n",
+ " !LE +5 Erhöht LE um 5 (bis zum Maximum)\n",
+ " !LE ++5 Erhöht LE um 5 (ignoriert Maximum)\n",
+ " !LE -5 Verringert LE um 5\n \n"
+ ]
+ },
+ {
+ "Name": "AE",
+ "Scope": "All",
+ "Brief": "Ändert Astralenergie",
+ "Description": [
+ "Mit !AE (oder !Asp) zeigt man die Astralenergie an, ändert sie, oder setzt sie auf einen neuen Wert\n\n",
+ " !AE Zeigt Astralenergie an\n",
+ " !AE 30 Setzt Asp auf 30\n",
+ " !AE +5 Erhöht Asp um 5 (bis zum Maximum)\n",
+ " !AE ++5 Erhöht Asp um 5 (ignoriert Maximum)\n",
+ " !AE -5 Verringert Asp um 5 (Minimum 0)\n"
+ ]
+ },
+ {
+ "Name": "Gm",
+ "Scope": "Meister",
+ "Brief": "Kontrolliere andere Charaktere",
+ "Description": [
+ "Mit !GM fürhrt man commands als eine andere Person aus.",
+ " !GM [charaktername] [command] [commandattribut] [mofifier]",
+ " Unterstützte [commands]'s:",
+ " !GM [name] LE",
+ " !GM [name] AE",
+ " !GM [name] Talent",
+ " !GM [name] Fernkampf",
+ " !GM [name] Eigenschaft",
+ " !GM [name] Zauber",
+ " !GM [name] Angriff",
+ " !GM [name] Parade"
+ ]
+ }
+] \ No newline at end of file
diff --git a/DSACore/PropertiesDSACore-DSA_Game-Characters-Character.json b/DSACore/PropertiesDSACore-DSA_Game-Characters-Character.json
new file mode 100644
index 0000000..fd387f5
--- /dev/null
+++ b/DSACore/PropertiesDSACore-DSA_Game-Characters-Character.json
@@ -0,0 +1,290 @@
+[
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 30,
+ "Lebenspunkte_Aktuell": 30,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 20,
+ "Astralpunkte_Aktuell": 20,
+ "Name": "Felis Exodus Schattenwald"
+ },
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 29,
+ "Lebenspunkte_Aktuell": 29,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 0,
+ "Astralpunkte_Aktuell": 0,
+ "Name": "Gardist"
+ },
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 31,
+ "Lebenspunkte_Aktuell": 31,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 0,
+ "Astralpunkte_Aktuell": 0,
+ "Name": "Hartmut Reiher"
+ },
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 21,
+ "Lebenspunkte_Aktuell": 21,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 35,
+ "Astralpunkte_Aktuell": 35,
+ "Name": "Helga vom Drachenei, Tausendsasserin"
+ },
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 25,
+ "Lebenspunkte_Aktuell": 25,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 0,
+ "Astralpunkte_Aktuell": 0,
+ "Name": "Krenko"
+ },
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 39,
+ "Lebenspunkte_Aktuell": 39,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 0,
+ "Astralpunkte_Aktuell": 0,
+ "Name": "Ledur Torfinson"
+ },
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 26,
+ "Lebenspunkte_Aktuell": 26,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 13,
+ "Astralpunkte_Aktuell": 13,
+ "Name": "Morla"
+ },
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 28,
+ "Lebenspunkte_Aktuell": 28,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 40,
+ "Astralpunkte_Aktuell": 40,
+ "Name": "Numeri Illuminus"
+ },
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 39,
+ "Lebenspunkte_Aktuell": 39,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 16,
+ "Astralpunkte_Aktuell": 16,
+ "Name": "Potus"
+ },
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 18,
+ "Lebenspunkte_Aktuell": 18,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 13,
+ "Astralpunkte_Aktuell": 13,
+ "Name": "Pump aus der Gosse"
+ },
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 34,
+ "Lebenspunkte_Aktuell": 34,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 17,
+ "Astralpunkte_Aktuell": 17,
+ "Name": "Rhoktar4"
+ },
+ {
+ "Eigenschaften": {},
+ "Talente": [],
+ "Zauber": [],
+ "Kampftalente": [],
+ "Vorteile": [],
+ "PropTable": {
+ "MU": "Mut",
+ "KL": "Klugheit",
+ "IN": "Intuition",
+ "CH": "Charisma",
+ "FF": "Fingerfertigkeit",
+ "GE": "Gewandtheit",
+ "KO": "Konstitution",
+ "KK": "Körperkraft"
+ },
+ "Lebenspunkte_Basis": 28,
+ "Lebenspunkte_Aktuell": 28,
+ "Ausdauer_Basis": 0,
+ "Ausdauer_Aktuell": 0,
+ "Astralpunkte_Basis": 43,
+ "Astralpunkte_Aktuell": 43,
+ "Name": "Volant"
+ }
+] \ No newline at end of file
diff --git a/DSACore/PropertiesNewtonsoft-Json-Linq-JProperty.json b/DSACore/PropertiesNewtonsoft-Json-Linq-JProperty.json
new file mode 100644
index 0000000..5b400e5
--- /dev/null
+++ b/DSACore/PropertiesNewtonsoft-Json-Linq-JProperty.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:2170",
+ "sslPort": 44365
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "api/commands",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "DSACore": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "api/commands",
+ "applicationUrl": "https://192.168.2.58:5001;http://192.168.2.58:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/DSACore/Startup.cs b/DSACore/Startup.cs
index 0628151..c189436 100644
--- a/DSACore/Startup.cs
+++ b/DSACore/Startup.cs
@@ -26,12 +26,23 @@ namespace DSACore
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
+ services.AddCors(options => options.AddPolicy("CorsPolicy",
+ builder =>
+ {
+ builder.WithOrigins("https://dsa.truekuehli.de")
+ .WithHeaders("Access-Control-Allow-Origin")
+ .AllowAnyHeader()
+ .AllowAnyMethod()
+ .AllowCredentials();
+ }));
+ /*
services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
- builder => builder.WithOrigins("https://console.firebase.google.com/project/heldenonline-4d828"));
+ builder => builder.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin().AllowCredentials()/*WithOrigins("https://dsa.truekuehli.de")#1#);
});
+*/
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSignalR();
@@ -49,9 +60,13 @@ namespace DSACore
app.UseHsts();
}
+ app.UseCors("CorsPolicy");
+
app.UseSignalR(routes => { routes.MapHub<ChatHub>("/chatHub"); });
- app.UseCors("AllowSpecificOrigin");
- app.UseHttpsRedirection();
+
+
+ //app.UseCors("AllowSpecificOrigin");
+ //app.UseHttpsRedirection();
app.UseMvc();
}
}
diff --git a/DiscoBot.sln b/DiscoBot.sln
index 7aab504..93659a9 100644
--- a/DiscoBot.sln
+++ b/DiscoBot.sln
@@ -7,11 +7,32 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscoBot", "DiscoBot\DiscoB
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZooBOTanica", "ZooBOTanica\ZooBOTanica.csproj", "{58917D99-DC94-4CDD-AD2B-C6E0BAFFCF47}"
EndProject
-Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "NodeJSServer", "NodeJSServer\NodeJSServer.njsproj", "{57064377-C08C-4218-9C55-0552D40F3877}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DSALib", "DSALib\DSALib.csproj", "{388DD4ED-29C4-4127-AC8F-34DD3FE9F9B0}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DSACore", "DSACore\DSACore.csproj", "{35A5E2CC-0FD4-4BC0-ACBF-38599CAED1C4}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DSACore", "DSACore\DSACore.csproj", "{35A5E2CC-0FD4-4BC0-ACBF-38599CAED1C4}"
+EndProject
+Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "dist", "WebInterface\NodeJSServer\dist\", "{3FEC1233-072D-4031-BBEB-B9804C58BD15}"
+ ProjectSection(WebsiteProperties) = preProject
+ TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5"
+ Debug.AspNetCompiler.VirtualPath = "/localhost_1915"
+ Debug.AspNetCompiler.PhysicalPath = "WebInterface\NodeJSServer\dist\"
+ Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_1915\"
+ Debug.AspNetCompiler.Updateable = "true"
+ Debug.AspNetCompiler.ForceOverwrite = "true"
+ Debug.AspNetCompiler.FixedNames = "false"
+ Debug.AspNetCompiler.Debug = "True"
+ Release.AspNetCompiler.VirtualPath = "/localhost_1915"
+ Release.AspNetCompiler.PhysicalPath = "WebInterface\NodeJSServer\dist\"
+ Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_1915\"
+ Release.AspNetCompiler.Updateable = "true"
+ Release.AspNetCompiler.ForceOverwrite = "true"
+ Release.AspNetCompiler.FixedNames = "false"
+ Release.AspNetCompiler.Debug = "False"
+ VWDPort = "1915"
+ SlnRelativePath = "WebInterface\NodeJSServer\dist\"
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FireBase", "FireBase\FireBase.csproj", "{87CC30E6-CBEA-4282-A3CC-FD5119A1993B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -27,10 +48,6 @@ Global
{58917D99-DC94-4CDD-AD2B-C6E0BAFFCF47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{58917D99-DC94-4CDD-AD2B-C6E0BAFFCF47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{58917D99-DC94-4CDD-AD2B-C6E0BAFFCF47}.Release|Any CPU.Build.0 = Release|Any CPU
- {57064377-C08C-4218-9C55-0552D40F3877}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {57064377-C08C-4218-9C55-0552D40F3877}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {57064377-C08C-4218-9C55-0552D40F3877}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {57064377-C08C-4218-9C55-0552D40F3877}.Release|Any CPU.Build.0 = Release|Any CPU
{388DD4ED-29C4-4127-AC8F-34DD3FE9F9B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{388DD4ED-29C4-4127-AC8F-34DD3FE9F9B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{388DD4ED-29C4-4127-AC8F-34DD3FE9F9B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -39,6 +56,14 @@ Global
{35A5E2CC-0FD4-4BC0-ACBF-38599CAED1C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35A5E2CC-0FD4-4BC0-ACBF-38599CAED1C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35A5E2CC-0FD4-4BC0-ACBF-38599CAED1C4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3FEC1233-072D-4031-BBEB-B9804C58BD15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3FEC1233-072D-4031-BBEB-B9804C58BD15}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3FEC1233-072D-4031-BBEB-B9804C58BD15}.Release|Any CPU.ActiveCfg = Debug|Any CPU
+ {3FEC1233-072D-4031-BBEB-B9804C58BD15}.Release|Any CPU.Build.0 = Debug|Any CPU
+ {87CC30E6-CBEA-4282-A3CC-FD5119A1993B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {87CC30E6-CBEA-4282-A3CC-FD5119A1993B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {87CC30E6-CBEA-4282-A3CC-FD5119A1993B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {87CC30E6-CBEA-4282-A3CC-FD5119A1993B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
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;
+ }
+ }
+}
diff --git a/WebInterface/NodeJSServer/NodeJSServer.njsproj b/WebInterface/NodeJSServer/NodeJSServer.njsproj
new file mode 100644
index 0000000..6181774
--- /dev/null
+++ b/WebInterface/NodeJSServer/NodeJSServer.njsproj
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{687040f4-1ffd-41aa-b4ca-e132a3d1fb15}</ProjectGuid>
+ <ProjectHome>.</ProjectHome>
+ <ProjectView>ShowAllFiles</ProjectView>
+ <StartupFile>webpack.config.js</StartupFile>
+ <WorkingDirectory>.</WorkingDirectory>
+ <OutputPath>.</OutputPath>
+ <ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
+ <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
+ <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)' == 'Debug'" />
+ <PropertyGroup Condition="'$(Configuration)' == 'Release'" />
+ <ItemGroup>
+ <Content Include="package-lock.json" />
+ <Content Include="package.json" />
+ <Content Include="webpack.config.js" />
+ <Content Include="dist\404.html" />
+ <Content Include="dist\index.html" />
+ <Content Include="dist\index.css" />
+ <Content Include="dist\chat.js" />
+ <Content Include="dist\index.js" />
+ <Content Include="src\index.js" />
+ <Content Include="dist\ressources\Logo768.png" />
+ <Content Include="dist\ressources\menu.png" />
+ <Content Include="dist\ressources\menu_close.png" />
+ <Content Include="src\modules\ui\backdrop.js" />
+ </ItemGroup>
+ <ItemGroup>
+ <Folder Include="dist" />
+ <Folder Include="dist\ressources" />
+ <Folder Include="src" />
+ <Folder Include="src\modules" />
+ <Folder Include="src\modules\ui" />
+ <Folder Include="src\style" />
+ <Folder Include="src\style\partials" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <!--Do not delete the following Import Project. While this appears to do nothing it is a marker for setting TypeScript properties before our import that depends on them.-->
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets" Condition="False" />
+ <Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsTools.targets" />
+ <ProjectExtensions>
+ <VisualStudio>
+ <FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
+ <WebProjectProperties>
+ <UseIIS>False</UseIIS>
+ <AutoAssignPort>True</AutoAssignPort>
+ <DevelopmentServerPort>0</DevelopmentServerPort>
+ <DevelopmentServerVPath>/</DevelopmentServerVPath>
+ <IISUrl>http://localhost:48022/</IISUrl>
+ <NTLMAuthentication>False</NTLMAuthentication>
+ <UseCustomServer>True</UseCustomServer>
+ <CustomServerUrl>http://localhost:1337</CustomServerUrl>
+ <SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
+ </WebProjectProperties>
+ </FlavorProperties>
+ <FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
+ <WebProjectProperties>
+ <StartPageUrl>
+ </StartPageUrl>
+ <StartAction>CurrentPage</StartAction>
+ <AspNetDebugging>True</AspNetDebugging>
+ <SilverlightDebugging>False</SilverlightDebugging>
+ <NativeDebugging>False</NativeDebugging>
+ <SQLDebugging>False</SQLDebugging>
+ <ExternalProgram>
+ </ExternalProgram>
+ <StartExternalURL>
+ </StartExternalURL>
+ <StartCmdLineArguments>
+ </StartCmdLineArguments>
+ <StartWorkingDirectory>
+ </StartWorkingDirectory>
+ <EnableENC>False</EnableENC>
+ <AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
+ </WebProjectProperties>
+ </FlavorProperties>
+ </VisualStudio>
+ </ProjectExtensions>
+</Project> \ No newline at end of file
diff --git a/WebInterface/NodeJSServer/dist/Web.config b/WebInterface/NodeJSServer/dist/Web.config
new file mode 100644
index 0000000..741b7d8
--- /dev/null
+++ b/WebInterface/NodeJSServer/dist/Web.config
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<configuration>
+ <!--
+ Eine Beschreibung der Änderungen von 'web.config' finden Sie unter 'http://go.microsoft.com/fwlink/?LinkId=235367'.
+
+ Die folgenden Attribute können für die <httpRuntime>-Kennung festgelegt werden.
+ <system.Web>
+ <httpRuntime targetFramework="4.5" />
+ </system.Web>
+ -->
+ <system.web>
+ <compilation debug="false" targetFramework="4.5"/>
+ <pages controlRenderingCompatibilityVersion="4.0"/>
+ </system.web>
+</configuration> \ No newline at end of file
diff --git a/WebInterface/NodeJSServer/dist/chat.js b/WebInterface/NodeJSServer/dist/chat.js
index 6832cc2..e9baff1 100644
--- a/WebInterface/NodeJSServer/dist/chat.js
+++ b/WebInterface/NodeJSServer/dist/chat.js
@@ -14,9 +14,19 @@ connection.start().catch(function (err) {
return console.error(err.toString());
});
+document.getElementById("loginButton").addEventListener("click", function (event) {
+ var group = document.getElementById("userInput").value;
+ var password = document.getElementById("messageInput").value;
+ connection.invoke("Login", group, password).catch(function (err) {
+ return console.error(err.toString());
+ });
+ event.preventDefault();
+});
+
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
+ var group = "TheCrew";
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
diff --git a/WebInterface/Web.config b/WebInterface/Web.config
new file mode 100644
index 0000000..741b7d8
--- /dev/null
+++ b/WebInterface/Web.config
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<configuration>
+ <!--
+ Eine Beschreibung der Änderungen von 'web.config' finden Sie unter 'http://go.microsoft.com/fwlink/?LinkId=235367'.
+
+ Die folgenden Attribute können für die <httpRuntime>-Kennung festgelegt werden.
+ <system.Web>
+ <httpRuntime targetFramework="4.5" />
+ </system.Web>
+ -->
+ <system.web>
+ <compilation debug="false" targetFramework="4.5"/>
+ <pages controlRenderingCompatibilityVersion="4.0"/>
+ </system.web>
+</configuration> \ No newline at end of file
diff --git a/ZooBOTanica/CritCreate.cs b/ZooBOTanica/CritCreate.cs
index f570c9e..08a6ff0 100644
--- a/ZooBOTanica/CritCreate.cs
+++ b/ZooBOTanica/CritCreate.cs
@@ -23,7 +23,7 @@ namespace ZooBOTanica
this.AllowDrop = true;
}
- private void Load(string path)
+ private new void Load(string path)
{
this.critter = Critter.Load(path);