diff --git a/.gitignore b/.gitignore index 01c7134..fea3cac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,16 @@ -[Ll]ibrary/ -[Tt]emp/ -[Oo]bj/ -[Bb]uild/ +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/Assets/AssetStoreTools* -# Autogenerated VS/MD solution and project files +# Visual Studio 2015 cache directory +/.vs/ + +# Autogenerated VS/MD/Consulo solution and project files +ExportedObj/ +.consulo/ *.csproj *.unityproj *.sln @@ -13,6 +20,8 @@ *.userprefs *.pidb *.booproj +*.svd +*.pdb # Unity3D generated meta files *.pidb.meta @@ -20,3 +29,12 @@ # Unity3D Generated File On Crash Reports sysinfo.txt + +# Builds +*.apk +*.unitypackage + +# IDEs +/.vscode +omnisharp.json +.editorconfig diff --git a/AppServiceClient.cs b/AppServiceClient.cs new file mode 100644 index 0000000..3d706a5 --- /dev/null +++ b/AppServiceClient.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; +using System.Collections; +using UnityEngine; + +namespace Azure.AppServices { + public sealed class AppServiceClient : ZumoClient { + + public AppServiceClient(string url) : base(url) { + } + + public static AppServiceClient Create(string account) { + string url = AppUrl(account); + return new AppServiceClient(url); + } + + private const string URI_API = "api"; + + public AppServiceTable GetTable(string tableName) where E : class { + return new AppServiceTable(tableName, this); + } + + /// + /// GET custom API + /// + public IEnumerator InvokeApi(string apiName, Action> callback = null) where T : new() { + return InvokeApi(apiName, Method.GET, callback); + } + + /// + /// Invokes custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE + /// + public IEnumerator InvokeApi(string apiName, Method httpMethod, Action> callback = null) where T : new() { + string url = ApiUrl(apiName); + Debug.Log(httpMethod.ToString() + " custom API Request Url: " + url); + ZumoRequest request = new ZumoRequest(url, httpMethod, true, User); + yield return request.Request.Send(); + request.ParseJson(callback); + } + + /// + /// Invokes custom API with body (of type B) and returning response (of type T) + /// + public IEnumerator InvokeApi(string apiName, Method httpMethod, B body, Action> callback = null) where T : new() { + string url = ApiUrl(apiName); + Debug.Log(httpMethod.ToString() + " custom API Request Url: " + url); + ZumoRequest request = new ZumoRequest(url, httpMethod, true, User); + request.AddBody(body); + yield return request.Request.Send(); + request.ParseJson(callback); + } + + private string ApiUrl(string apiName) { + return string.Format("{0}/{1}/{2}", Url, URI_API, apiName); + } + + } +} diff --git a/IAppServiceClient.cs b/IAppServiceClient.cs new file mode 100644 index 0000000..c67bf08 --- /dev/null +++ b/IAppServiceClient.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; +using System.Collections; + +namespace Azure.AppServices { + public interface IAppServiceClient : IZumoClient { + AppServiceTable GetTable(string tableName) where E : class; + + /// + /// Invokes a custom API using GET HTTP method + /// + IEnumerator InvokeApi(string apiName, Action> callback = null) where T : new(); + + /// + /// Invokes a custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE + /// + IEnumerator InvokeApi(string apiName, Method httpMethod, Action> callback = null) where T : new(); + + /// + /// Invokes a custom API with body object + /// + IEnumerator InvokeApi(string apiName, Method httpMethod, B body, Action> callback = null) where T : new(); + } +} diff --git a/IZumoClient.cs b/IZumoClient.cs new file mode 100644 index 0000000..77c94b3 --- /dev/null +++ b/IZumoClient.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; +using System.Collections; + +namespace Azure.AppServices { + public interface IZumoClient { + + AuthenticatedUser User { get; } + + IEnumerator LoginWithFacebook(string accessToken, Action> callback = null); + + IEnumerator LoginWithTwitter(string accessToken, string accessTokenSecret, Action> callback = null); + + IEnumerator LoginWithGoogle(string accessToken, string idToken, Action> callback = null); + + IEnumerator LoginWithMicrosoftAccount(string accessToken, Action> callback = null); + + IEnumerator LoginWithAAD(string accessToken, Action> callback = null); + + IEnumerator Logout(Action> callback = null); + } +} diff --git a/README.md b/README.md index 387b2ef..2b30a91 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,19 @@ -# Azure App Services for Unity3d -For game developers looking to use Azure App Services (previously Mobile Services) in their Unity project. This App Services REST API library for Unity is quite similar in structure to the Azure Mobile Services SDK so if you've used that before you should feel relatively at home. +# Azure App Services for Unity +For Unity developers looking to use Azure App Services (previously Mobile Services) in their Unity game / app. -## Prerequisites +## What's new +Please be aware this latest update of the library brings a number of changes to the namespace, some class names and method calls have been updated as Mobile Services is replaced by App Services. There is also a dependency on a shared [RESTClient for Unity](https://github.com/Unity3dAzure/RESTClient) so that multiple Unity libraries for Azure can be supported without adding duplicate scripts. + +## External dependencies +**First download the shared [REST Client library for Unity](https://github.com/Unity3dAzure/RESTClient) and extract the contents into your Unity project "Assets" folder.** +* [RESTClient](https://github.com/Unity3dAzure/RESTClient) + +## Requirements Requires Unity v5.3 or greater as [UnityWebRequest](https://docs.unity3d.com/Manual/UnityWebRequest.html) and [JsonUtility](https://docs.unity3d.com/ScriptReference/JsonUtility.html) features are used. Unity will be extending platform support for UnityWebRequest so keep Unity up to date if you need to support these additional platforms. ## How to setup App Services with a new Unity project -1. [Download AppServices](https://github.com/Unity3dAzure/AppServices/archive/master.zip) - * Copy 'AppServices' into project `Assets` folder. +1. [Download AppServices](https://github.com/Unity3dAzure/AppServices/archive/master.zip) and [REST Client](https://github.com/Unity3dAzure/RESTClient/archive/master.zip) for Unity. + * Copy 'AppServices' and 'RESTClient' into project `Assets` folder. 2. Create an Azure App Service [Mobile App](https://portal.azure.com) * Create a Table (using Easy Tables) for app data. @@ -15,94 +22,118 @@ Try the [Azure App Services Demos](https://github.com/Unity3dAzure/AppServicesDe For detailed instructions read my developer blog on [how to setup Azure App Services and Unity demo project](http://www.deadlyfingers.net/azure/azure-app-services-for-unity3d/). ## Supported Features -### MobileServiceClient +### AppServiceClient API API | Description --- | ----------- -Login | Client-directed login using access token. +LoginWithFacebook | Client-directed login using user access token. +LoginWithTwitter | Client-directed login using access token and access token secret. +LoginWithGoogle | Client-directed login using access token and id token. +LoginWithMicrosoftAccount | Client-directed login using access token. +LoginWithAAD | Client-directed login using access token. +Logout | Logout current user InvokeApi | Invoke custom API (Easy API) using GET, POST, PUT, PATCH or DELETE -### MobileServiceTable +### AppServiceTable API API | Description --- | ----------- Insert | Create a new item. Read | Get a list of items. Update | Update an item’s data using id property. -Delete | Delete an item using id property. +Delete | Delete an item using id property. Query | Get a list of results using a custom query. Lookup | Get an item’s data using id property. -### [MobileServiceClient Interface](https://github.com/Unity3dAzure/AppServices/blob/master/client/IAzureMobileServiceClient.cs) - Login(MobileServiceAuthenticationProvider provider, string token, Action> callback = null); - InvokeApi(string apiName, Action> callback = null) where T : new(); - InvokeApi(string apiName, Method httpMethod, Action> callback = null) where T : new(); - InvokeApi(string apiName, Method httpMethod, T body, Action> callback = null) where T : new(); +### [AppServiceClient Interface](https://github.com/Unity3dAzure/AppServices/blob/master/IAppServiceClient.cs) + LoginWithFacebook(string accessToken, Action> callback = null); + LoginWithTwitter(string accessToken, string accessTokenSecret, Action> callback = null); + LoginWithGoogle(string accessToken, string idToken,Action> callback = null); + LoginWithMicrosoftAccount(string accessToken,Action> callback = null); + LoginWithAAD(string accessToken, Action> callback = null); + Logout(Action> callback = null); + +### [AppServiceClient (Easy APIs) Interface](https://github.com/Unity3dAzure/AppServices/blob/master/IAppServiceClient.cs) + InvokeApi (string apiName, Action> callback = null) where T : new(); + InvokeApi (string apiName, Method httpMethod, Action> callback = null) where T : new(); + InvokeApi (string apiName, Method httpMethod, B body, Action> callback = null) where T : new(); -### [MobileServiceTable Interface](https://github.com/Unity3dAzure/AppServices/blob/master/table/IAzureMobileServiceTable.cs) +### [AppServiceTable (Easy Tables) Interface](https://github.com/Unity3dAzure/AppServices/blob/master/table/IAppServiceTable.cs) Insert(T item, Action> callback = null) where T : new(); Read(Action> callback = null) where T : new(); Update(T item, Action> callback = null) where T : new(); Delete(string id, Action> callback = null) where T : new(); - Query(CustomQuery query, Action> callback = null) where T : new(); - Query(CustomQuery query, Action> callback = null) where T : INestedResults, new(); + Query(TableQuery query, Action> callback = null) where T : new(); + Query(TableQuery query, Action> callback = null) where T : INestedResults, new(); Lookup(string id, Action> callback = null) where T : new(); ## Sample usage + - Data model for **TodoItem.cs** ``` +using Azure.AppServices; +[System.Serializable] +public class TodoItem : DataModel { + public string text; + public bool complete; +} +``` + + - Unity script +``` +using System.Collections +using System.Collections.Generic; using UnityEngine; using System; -using System.Net; -using System.Collections.Generic; -using Unity3dAzure.AppServices; - +using RESTClient; +using Azure.AppServices; ``` ``` -private MobileServiceClient _client; -private MobileServiceTable _table; +private AppServiceClient _client; +private AppServiceTable _table; ``` ``` void Start () { - _client = new MobileServiceClient(appUrl); // <- add your app url here. - _table = _client.GetTable("TodoItem"); + _client = AppServiceClient.Create("unityapp"); // <- add your App Service account here! + _table = _client.GetTable("TodoItem"); + ReadItems(); } ``` ``` private void ReadItems() { - StartCoroutine( _table.Read(OnReadItemsCompleted) ); + StartCoroutine( _table.Read(OnReadItemsCompleted) ); } private void OnReadItemsCompleted(IRestResponse response) { - if (!response.IsError) { - Debug.Log ("OnReadCompleted: " + response.Url + " data: " + response.Content); - TodoItem[] items = response.Data; - Debug.Log ( "Todo items count: " + items.Count); - } else { - Debug.LogWarning ("Read Error Status:" + response.StatusCode + " Url: " + response.Url); - } + if (!response.IsError) { + Debug.Log ("OnReadCompleted: " + response.Url + " data: " + response.Content); + TodoItem[] items = response.Data; + Debug.Log ( "Todo items count: " + items.Length); + } else { + Debug.LogWarning ("Read Error Status:" + response.StatusCode + " Url: " + response.Url); + } } ``` ## Known issues * There is an issue with [PATCH on Android using UnityWebRequest with Azure App Services](http://answers.unity3d.com/questions/1230067/trying-to-use-patch-on-a-unitywebrequest-on-androi.html). Android won't recognize the "PATCH" http method currently required to update an item in App Services. One workaround is to enable the `X-HTTP-Method-Override` header. Here's the quick fix for App Services running node backend: - 1. Install the "method-override" package. + 1. Install the "method-override" package. ``` npm install method-override --save - ``` - 2. In 'app.js' file insert: ``` - var methodOverride = require('method-override'); - // after the line "var app = express();" add + 2. In 'app.js' file insert: + ``` + var methodOverride = require('method-override'); + // after the line "var app = express();" add app.use(methodOverride('X-HTTP-Method-Override')); ``` This will enable PATCH requests to be sent on Android. ## Supported platforms -Will work on all the platforms [UnityWebRequest](https://docs.unity3d.com/Manual/UnityWebRequest.html) supports including: -* Unity Editor and Standalone players +Intended to work on all the platforms [UnityWebRequest](https://docs.unity3d.com/Manual/UnityWebRequest.html) supports including: +* Unity Editor (Mac/PC) and Standalone players * iOS * Android * Windows -Questions or tweet #Azure #GameDev [@deadlyfingers](https://twitter.com/deadlyfingers) +Questions or tweet [@deadlyfingers](https://twitter.com/deadlyfingers) diff --git a/ZumoClient.cs b/ZumoClient.cs new file mode 100644 index 0000000..d233050 --- /dev/null +++ b/ZumoClient.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using UnityEngine; +using System.Net; +using UnityEngine.Assertions; + +namespace Azure.AppServices { + public abstract class ZumoClient : RestClient, IZumoClient { + + public AuthenticatedUser User { get; private set; } + + public ZumoClient(string url) : base(url) { + } + + public static string AppUrl(string account) { + return string.Format("https://{0}.azurewebsites.net", account); + } + + /// + /// Client-directed single sign on for Facebook (using access token) + /// + public IEnumerator LoginWithFacebook(string accessToken, Action> callback = null) { + return Login(AuthenticationProvider.Facebook, accessToken, callback); + } + + /// + /// Client-directed single sign on for Twitter (using access token and access token secret) + /// + public IEnumerator LoginWithTwitter(string accessToken, string accessTokenSecret, Action> callback = null) { + string url = AppServiceAuthenticationProviderUrl(AuthenticationProvider.Twitter); + var request = new ZumoRequest(url, Method.POST, false); + request.AddBodyAccessTokenSecret(accessToken, accessTokenSecret); + yield return request.Request.Send(); + LoggedIn(request, callback); + } + + /// + /// Client-directed single sign on for Google+ (using access token and id token) + /// + public IEnumerator LoginWithGoogle(string accessToken, string idToken, Action> callback = null) { + string url = AppServiceAuthenticationProviderUrl(AuthenticationProvider.Google); + var request = new ZumoRequest(url, Method.POST, false); + request.AddBodyAccessTokenId(accessToken, idToken); + yield return request.Request.Send(); + LoggedIn(request, callback); + } + + /// + /// Client-directed single sign on for Microsoft Account (using access token) + /// + public IEnumerator LoginWithMicrosoftAccount(string accessToken, Action> callback = null) { + return Login(AuthenticationProvider.MicrosoftAccount, accessToken, callback); + } + + /// + /// Client-directed single sign on for Azure Active Directory (using access token) + /// + public IEnumerator LoginWithAAD(string accessToken, Action> callback = null) { + return Login(AuthenticationProvider.AAD, accessToken, callback); + } + + private IEnumerator Login(AuthenticationProvider authenticationProvider, + string accessToken, + Action> callback = null) { + string url = AppServiceAuthenticationProviderUrl(authenticationProvider); + var request = new ZumoRequest(url, Method.POST, false); + request.AddBodyAccessToken(accessToken); + yield return request.Request.Send(); + LoggedIn(request, callback); + } + + public IEnumerator Logout(Action> callback = null) { + if (User == null) { + Debug.LogWarning("Error, requires user login."); + yield break; + } + string url = string.Format("{0}/.auth/logout", Url); + var request = new ZumoRequest(url, Method.POST, false, User); + yield return request.Request.Send(); + if (callback == null) { + yield break; + } + HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), request.Request.responseCode.ToString()); + RestResponse response = new RestResponse(statusCode, request.Request.url, request.Request.downloadHandler.text); + if (!statusCode.Equals(HttpStatusCode.OK)) { + Debug.LogWarning("Error, logout request failed."); + yield break; + } + // Detect result of logout webpage (using title tag to verify sign out success) with callback response + var match = Regex.Match(request.Request.downloadHandler.text, @"(.+)<\/title>", RegexOptions.IgnoreCase); + if (match.Groups.Count == 2 && !string.IsNullOrEmpty(match.Groups[1].Value)) { + string title = match.Groups[1].Value; + Assert.IsTrue(string.Equals(title, "You have been signed out")); + response = new RestResponse<string>(statusCode, request.Request.url, title); + } + callback(response); + User = null; + request.Dispose(); + } + + private string AppServiceAuthenticationProviderUrl(AuthenticationProvider authenticationProvider) { + return string.Format("{0}/.auth/login/{1}", Url, authenticationProvider.ToString().ToLower()); + } + + private void LoggedIn(ZumoRequest request, Action<IRestResponse<AuthenticatedUser>> callback = null) { + string message = request.Request.downloadHandler.text; + IRestResponse<AuthenticatedUser> response = request.ParseJson<AuthenticatedUser>(); + if (!response.IsError) { + User = response.Data; + if (callback != null) { + callback(response); + } + } else { + Debug.LogWarning("Login error message:" + message); + } + request.Dispose(); + } + + } +} diff --git a/authentication/AuthenticationProvider.cs b/authentication/AuthenticationProvider.cs new file mode 100644 index 0000000..597f7f9 --- /dev/null +++ b/authentication/AuthenticationProvider.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + // App Service authentication identity providers: Microsoft Account, Google Plus, Twitter, Facebook, Azure Active Directory + public enum AuthenticationProvider { + MicrosoftAccount = 0, + Google = 1, + Twitter = 2, + Facebook = 3, + AAD = 4 + } +} diff --git a/authentication/MobileServiceAuthenticationProvider.cs b/authentication/MobileServiceAuthenticationProvider.cs deleted file mode 100644 index ede255a..0000000 --- a/authentication/MobileServiceAuthenticationProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Unity3dAzure.AppServices -{ - public enum MobileServiceAuthenticationProvider - { - MicrosoftAccount = 0, - Google = 1, - Twitter = 2, - Facebook = 3, - WindowsAzureActiveDirectory = 4 - } -} \ No newline at end of file diff --git a/client/IAzureMobileServiceClient.cs b/client/IAzureMobileServiceClient.cs deleted file mode 100644 index 6d0de2b..0000000 --- a/client/IAzureMobileServiceClient.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections; - -namespace Unity3dAzure.AppServices -{ - public interface IAzureMobileServiceClient - { - MobileServiceTable<E> GetTable<E> (string tableName) where E : class; - - /// <summary> - /// Client-directed login (sdk) for single sign on - /// https://msdn.microsoft.com/en-us/library/azure/jj710106.aspx - /// </summary> - IEnumerator Login (MobileServiceAuthenticationProvider provider, string token, Action<IRestResponse<MobileServiceUser>> callback = null); - - /// <summary> - /// TODO: Service-directed login (webview) - /// </summary> - //void Login(MobileServiceAuthenticationProvider provider); - - /// <summary> - /// Invokes a custom API using GET HTTP method - /// </summary> - IEnumerator InvokeApi<T> (string apiName, Action<IRestResponse<T>> callback = null) where T : new(); - - /// <summary> - /// Invokes a custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE - /// </summary> - IEnumerator InvokeApi<T> (string apiName, Method httpMethod, Action<IRestResponse<T>> callback = null) where T : new(); - - /// <summary> - /// Invokes a custom API with body object - /// </summary> - IEnumerator InvokeApi<T> (string apiName, Method httpMethod, T body, Action<IRestResponse<T>> callback = null) where T : new(); - } -} \ No newline at end of file diff --git a/client/MobileServiceClient.cs b/client/MobileServiceClient.cs deleted file mode 100644 index 7bdf401..0000000 --- a/client/MobileServiceClient.cs +++ /dev/null @@ -1,127 +0,0 @@ -using UnityEngine; -using System.Collections; -using System.Collections.Generic; -using System; -using System.Text.RegularExpressions; - - -#if !NETFX_CORE || UNITY_ANDROID -using System.Net; -using System.Security.Cryptography.X509Certificates; -using System.Net.Security; -#endif - -namespace Unity3dAzure.AppServices -{ - public class MobileServiceClient : IAzureMobileServiceClient - { - public string AppUrl { get; private set; } - - public MobileServiceUser User { get; set; } - - public const string URI_API = "api/"; - - /// <summary> - /// Creates a new RestClient using Azure App Service's Application Url - /// </summary> - public MobileServiceClient (string appUrl) - { - AppUrl = HttpsUri (appUrl); - Debug.Log ("App Url: " + AppUrl); - - // required for running in Windows and Android - #if !NETFX_CORE || UNITY_ANDROID - Debug.Log ("ServerCertificateValidation"); - ServicePointManager.ServerCertificateValidationCallback = RemoteCertificateValidationCallback; - #endif - } - - public override string ToString () - { - return this.AppUrl; - } - - public MobileServiceTable<E> GetTable<E> (string tableName) where E : class - { - return new MobileServiceTable<E> (tableName, this); - } - - /// <summary> - /// Client-directed single sign on (POST with access token) - /// </summary> - public IEnumerator Login (MobileServiceAuthenticationProvider provider, string token, Action<IRestResponse<MobileServiceUser>> callback = null) - { - string p = provider.ToString ().ToLower (); - string url = string.Format ("{0}/.auth/login/{1}", AppUrl, p); - Debug.Log ("Login Request Url: " + url + " access token: " + token); - ZumoRequest request = new ZumoRequest (this, url, Method.POST); - request.AddBodyAccessToken (token); - yield return request.request.Send (); - request.ParseJson<MobileServiceUser> (callback); - } - - /// <summary> - /// TODO: Service login (using GET via webview) - /// </summary> - /* - public void Login(MobileServiceAuthenticationProvider provider) - { - Debug.Log("TODO"); - } - //*/ - - /// <summary> - /// GET custom API - /// </summary> - public IEnumerator InvokeApi<T> (string apiName, Action<IRestResponse<T>> callback = null) where T : new() - { - return InvokeApi<T> (apiName, Method.GET, callback); - } - - /// <summary> - /// Invokes custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE - /// </summary> - public IEnumerator InvokeApi<T> (string apiName, Method httpMethod, Action<IRestResponse<T>> callback = null) where T : new() - { - string url = string.Format ("{0}/{1}{2}", AppUrl, URI_API, apiName); - Debug.Log (httpMethod.ToString () + " custom API Request Url: " + url); - ZumoRequest request = new ZumoRequest (this, url, httpMethod); - yield return request.request.Send (); - request.ParseJson<T> (callback); - } - - /// <summary> - /// Invokes custom API with body - /// </summary> - public IEnumerator InvokeApi<T> (string apiName, Method httpMethod, T body, Action<IRestResponse<T>> callback = null) where T : new() - { - string url = string.Format ("{0}/{1}{2}", AppUrl, URI_API, apiName); - Debug.Log (httpMethod.ToString () + " custom API Request Url: " + url); - ZumoRequest request = new ZumoRequest (this, url, httpMethod); - request.AddBody<T> (body); - yield return request.request.Send (); - request.ParseJson<T> (callback); - } - - /// <summary> - /// When you copy the URL from the Azure Portal it is 'http' by default, but it needs to be 'https' for post - /// </summary> - private static string HttpsUri (string appUrl) - { - return Regex.Replace (appUrl, "(?si)^http://", "https://").TrimEnd ('/'); - } - - #if !NETFX_CORE || UNITY_ANDROID - private bool RemoteCertificateValidationCallback (System.Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) - { - // Check the certificate to see if it was issued from Azure - if (certificate.Subject.Contains ("azurewebsites.net")) { - return true; - } else { - return false; - } - } - #endif - - } -} diff --git a/helpers/DataHelper.cs b/helpers/DataHelper.cs deleted file mode 100644 index d546cc6..0000000 --- a/helpers/DataHelper.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Text; - -namespace Unity3dAzure.AppServices -{ - public class DataHelper - { - - public static byte[] ToUTF8Bytes (string utf8String) - { - return Encoding.UTF8.GetBytes (utf8String); - } - - public static string ToUTF8String (byte[] bytes) - { - return Encoding.UTF8.GetString (bytes); - } - - } -} \ No newline at end of file diff --git a/helpers/JsonHelper.cs b/helpers/JsonHelper.cs deleted file mode 100644 index 4f3d564..0000000 --- a/helpers/JsonHelper.cs +++ /dev/null @@ -1,107 +0,0 @@ -#pragma warning disable 0649 // ignores warning: array "is never assigned to, and will always have its default value 'null'" -using UnityEngine; -using System; -using System.Text.RegularExpressions; -#if NETFX_CORE -using Windows.Data.Json; -using System.Collections.Generic; -using System.Linq; -#endif - -namespace Unity3dAzure.AppServices -{ - /// <summary> - /// Wrapper work-around for json array described on https://forum.unity3d.com/threads/how-to-load-an-array-with-jsonutility.375735/ - /// </summary> - [Serializable] - internal class Wrapper<T> - { - public T[] array; - } - - public class JsonHelper - { - /// <summary> - /// Work-around to parse json array - /// </summary> - public static T[] FromJsonArray<T>(string json) - { - // Work-around for JsonUtility array serialization issues in Windows Store Apps. -#if NETFX_CORE - JsonArray jsonArray = new JsonArray(); - if (JsonArray.TryParse(json, out jsonArray)) - { - return GetArray<T>(jsonArray); - } - Debug.LogWarning("Failed to parse json array of type:" + typeof(T).ToString() ); - return default(T[]); -#endif - string newJson = "{\"array\":" + json + "}"; - Wrapper<T> wrapper = new Wrapper<T>(); - try - { - wrapper = JsonUtility.FromJson<Wrapper<T>>(newJson); - } - catch (Exception e) - { - Debug.LogWarning("Failed to parse json array of type:" + typeof(T).ToString() + " Exception message: " + e.Message); - return default(T[]); - } - return wrapper.array; - } - - public static N FromJsonNestedArray<T,N>(string json, string namedArray) where N : INestedResults<T>, new() - { -#if NETFX_CORE - JsonObject jsonObject = new JsonObject(); - if (JsonObject.TryParse(json, out jsonObject)) - { - JsonArray jsonArray = jsonObject.GetNamedArray(namedArray); - T[] array = GetArray<T>(jsonArray); - N nestedResults = new N(); //NestedResults<T> nestedResults = new NestedResults<T>(array); - nestedResults.SetArray(array); - - string namedCount = nestedResults.GetCountField(); - uint count = Convert.ToUInt32( jsonObject.GetNamedNumber(namedCount) ); - nestedResults.SetCount(count); - - return nestedResults; - } else { - Debug.LogWarning("Failed to parse json nested array of type:" + typeof(T).ToString()); - return default(N); - } -#endif - N results = JsonUtility.FromJson<N>(json); // NestedResults<T> nestedResults = JsonUtility.FromJson<NestedResults<T>(json); - return results; - } - -#if NETFX_CORE - private static T[] GetArray<T>(JsonArray array) - { - List<T> list = new List<T>(); - foreach (var x in array) - { - try - { - T item = JsonUtility.FromJson<T>(x.ToString()); - list.Add(item); - } - catch (Exception e) - { - Debug.LogWarning("Failed to parse json of type:" + typeof(T).ToString() + " Exception message: " + e.Message + " json:'" + x.ToString() + "'"); - } - } - return list.ToArray(); - } -#endif - - /// <summary> - /// Workaround to only exclude Data Model's read only system properties being returned as json object. Unfortunately there is no JsonUtil attribute to do this as [NonSerialized] will just ignore the properties completely (both in and out). - /// </summary> - public static string ToJsonExcludingSystemProperties(object obj) - { - string jsonString = JsonUtility.ToJson(obj); - return Regex.Replace(jsonString, "(?i)(\\\"id\\\":\\\"\\\",)?(\\\"createdAt\\\":\\\"[0-9TZ:.-]*\\\",)?(\\\"updatedAt\\\":\\\"[0-9TZ:.-]*\\\",)?(\\\"version\\\":\\\"[A-Z0-9=]*\\\",)?(\\\"deleted\\\":(true|false),)?(\\\"ROW_NUMBER\\\":\\\"[0-9]*\\\",)?", ""); - } - } -} \ No newline at end of file diff --git a/helpers/Model.cs b/helpers/Model.cs deleted file mode 100644 index eef4235..0000000 --- a/helpers/Model.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Reflection; - -namespace Unity3dAzure.AppServices -{ - /// <summary> - /// Helper methods to check and get object properties - /// </summary> - public class Model - { - public static bool HasProperty (object obj, string propertyName) - { - return GetProperty (obj, propertyName) != null; - } - - public static PropertyInfo GetProperty (object obj, string propertyName) - { - #if NETFX_CORE - return obj.GetType().GetTypeInfo().GetDeclaredProperty(propertyName); // GetProperty for UWP - #else - return obj.GetType ().GetProperty (propertyName); - #endif - } - - public static bool HasField (object obj, string fieldName) - { - return GetField (obj, fieldName) != null; - } - - public static FieldInfo GetField (object obj, string fieldName) - { - #if NETFX_CORE - return obj.GetType().GetTypeInfo().GetDeclaredField(fieldName); // GetField for UWP - #else - return obj.GetType ().GetField (fieldName); - #endif - } - } -} \ No newline at end of file diff --git a/http/IRestResponse.cs b/http/IRestResponse.cs deleted file mode 100644 index d7da69d..0000000 --- a/http/IRestResponse.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Net; - -namespace Unity3dAzure.AppServices -{ - public interface IRestResponse<T> - { - bool IsError { get; } - - string ErrorMessage { get; } - - string Url { get; } - - HttpStatusCode StatusCode { get; } - - string Content { get; } - - T Data { get; } - } -} - diff --git a/http/Method.cs b/http/Method.cs deleted file mode 100644 index b2d5e6f..0000000 --- a/http/Method.cs +++ /dev/null @@ -1,13 +0,0 @@ -using UnityEngine; -using System.Collections; - -namespace Unity3dAzure.AppServices -{ - public enum Method - { - GET, - POST, - PATCH, - DELETE - } -} \ No newline at end of file diff --git a/http/RestRequest.cs b/http/RestRequest.cs deleted file mode 100644 index c7af339..0000000 --- a/http/RestRequest.cs +++ /dev/null @@ -1,172 +0,0 @@ -using UnityEngine; -using UnityEngine.Networking; -using System; -using System.Net; - -namespace Unity3dAzure.AppServices -{ - public abstract class RestRequest : IDisposable - { - public UnityWebRequest request { get; private set; } - - public RestRequest (string url, Method method) - { - request = new UnityWebRequest (url, method.ToString ()); - request.downloadHandler = new DownloadHandlerBuffer (); - } - - public void AddHeader (string key, string value) - { - request.SetRequestHeader (key, value); - } - - public void AddBody (byte[] bytes, string contentType) - { - if (request.uploadHandler != null) { - Debug.LogWarning ("Request body can only be set once"); - return; - } - request.uploadHandler = new UploadHandlerRaw (bytes); - request.uploadHandler.contentType = contentType; - } - - public virtual void AddBody<T> (T data, string contentType = "application/json; charset=utf-8") - { - string jsonString = JsonUtility.ToJson (data); - byte[] bytes = DataHelper.ToUTF8Bytes (jsonString); - this.AddBody (bytes, contentType); - } - - #region Response and json object parsing - - private RestResult<T> GetRestResult<T>() - { - HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), request.responseCode.ToString()); - RestResult<T> result = new RestResult<T>(statusCode); - - if (result.IsError) - { - result.ErrorMessage = "Response failed with status: " + statusCode.ToString(); - return result; - } - - if (string.IsNullOrEmpty(request.downloadHandler.text)) - { - result.IsError = true; - result.ErrorMessage = "Response has empty body"; - return result; - } - - return result; - } - - /// <summary> - /// Shared method to return response result whether an object or array of objects - /// </summary> - private RestResult<T> TryParseJsonArray<T> () - { - RestResult<T> result = GetRestResult<T>(); - // try parse an array of objects - try - { - result.AnArrayOfObjects = JsonHelper.FromJsonArray<T>(request.downloadHandler.text); - } - catch (Exception e) - { - result.IsError = true; - result.ErrorMessage = "Failed to parse an array of objects of type: " + typeof(T).ToString() + " Exception message: " + e.Message; - } - return result; - } - - private RestResult<T> TryParseJson<T>() - { - RestResult<T> result = GetRestResult<T>(); - // try parse an object - try - { - result.AnObject = JsonUtility.FromJson<T>(request.downloadHandler.text); - } - catch (Exception e) - { - result.IsError = true; - result.ErrorMessage = "Failed to parse object of type: " + typeof(T).ToString() + " Exception message: " + e.Message; - } - return result; - } - - /// <summary> - /// Parses object with T data = JsonUtil.FromJson<T>, then callback RestResponse<T> - /// </summary> - public void ParseJson<T> (Action<IRestResponse<T>> callback = null) - { - RestResult<T> result = TryParseJson<T> (); - - if (result.IsError) { - Debug.LogWarning ("Response error status:" + result.StatusCode + " code:" + request.responseCode + " error:" + result.ErrorMessage + " request url:" + request.url); - callback (new RestResponse<T> (result.ErrorMessage, result.StatusCode, request.url, request.downloadHandler.text)); - } else { - callback (new RestResponse<T> (result.StatusCode, request.url, request.downloadHandler.text, result.AnObject)); - } - this.Dispose(); - } - - /// <summary> - /// Parses array of objects with T[] data = JsonHelper.GetJsonArray<T>, then callback RestResponse<T[]> - /// </summary> - public void ParseJsonArray<T> (Action<IRestResponse<T[]>> callback = null) - { - RestResult<T> result = TryParseJsonArray<T> (); - - if (result.IsError) { - Debug.LogWarning ("Response error status:" + result.StatusCode + " code:" + request.responseCode + " error:" + result.ErrorMessage + " request url:" + request.url); - callback (new RestResponse<T[]> (result.ErrorMessage, result.StatusCode, request.url, request.downloadHandler.text)); - } else { - callback (new RestResponse<T[]> (result.StatusCode, request.url, request.downloadHandler.text, result.AnArrayOfObjects)); - } - this.Dispose(); - } - - // *WSA - private RestResult<N> TryParseJsonNestedArray<T,N>(string namedArray) where N : INestedResults<T>, new() - { - RestResult<N> result = GetRestResult<N>(); - // try parse an object - try - { - result.AnObject = JsonHelper.FromJsonNestedArray<T,N>(request.downloadHandler.text, namedArray); //JsonUtility.FromJson<N>(request.downloadHandler.text); - } - catch (Exception e) - { - result.IsError = true; - result.ErrorMessage = "Failed to parse object of type: " + typeof(N).ToString() + " Exception message: " + e.Message; - } - return result; - } - - // Work-around for nested array - public void ParseJsonNestedArray<T,N>(string namedArray, Action<IRestResponse<N>> callback = null) where N : INestedResults<T>, new() - { - RestResult<N> result = TryParseJsonNestedArray<T,N>(namedArray); - - if (result.IsError) - { - Debug.LogWarning("Response error status:" + result.StatusCode + " code:" + request.responseCode + " error:" + result.ErrorMessage + " request url:" + request.url); - callback(new RestResponse<N>(result.ErrorMessage, result.StatusCode, request.url, request.downloadHandler.text)); - } - else - { - callback(new RestResponse<N>(result.StatusCode, request.url, request.downloadHandler.text, result.AnObject)); - } - this.Dispose(); - } - - public void Dispose() - { - request.Dispose(); // request completed, clean-up resources - } - - #endregion - - } -} diff --git a/http/RestResponse.cs b/http/RestResponse.cs deleted file mode 100644 index 989532b..0000000 --- a/http/RestResponse.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Net; - -namespace Unity3dAzure.AppServices -{ - public abstract class Response - { - public bool IsError { get; set; } - - public string ErrorMessage { get; set; } - - public string Url { get; set; } - - public HttpStatusCode StatusCode { get; set; } - - public string Content { get; set; } - - protected Response (HttpStatusCode statusCode) - { - this.StatusCode = statusCode; - this.IsError = !((int)statusCode >= 200 && (int)statusCode < 300); - } - - // success - protected Response (HttpStatusCode statusCode, string url, string text) - { - this.IsError = false; - this.Url = url; - this.ErrorMessage = null; - this.StatusCode = statusCode; - this.Content = text; - } - - // failure - protected Response (string error, HttpStatusCode statusCode, string url, string text) - { - this.IsError = true; - this.Url = url; - this.ErrorMessage = error; - this.StatusCode = statusCode; - this.Content = text; - } - } - - public sealed class RestResponse : Response - { - // success - public RestResponse (HttpStatusCode statusCode, string url, string text) : base (statusCode, url, text) - { - } - - // failure - public RestResponse (string error, HttpStatusCode statusCode, string url, string text) : base (error, statusCode, url, text) - { - } - } - - public sealed class RestResponse<T> : Response, IRestResponse<T> - { - public T Data { get; set; } - - // success - public RestResponse (HttpStatusCode statusCode, string url, string text, T data) : base (statusCode, url, text) - { - this.Data = data; - } - - // failure - public RestResponse (string error, HttpStatusCode statusCode, string url, string text) : base (error, statusCode, url, text) - { - } - } - - /// <summary> - /// Parsed JSON result could either be an object or an array of objects - /// </summary> - internal sealed class RestResult<T> : Response - { - public T AnObject { get; set; } - - public T[] AnArrayOfObjects { get; set; } - - public RestResult (HttpStatusCode statusCode) : base (statusCode) - { - } - } - -} - diff --git a/http/ZumoRequest.cs b/http/ZumoRequest.cs index 6409817..04f5e7b 100644 --- a/http/ZumoRequest.cs +++ b/http/ZumoRequest.cs @@ -1,33 +1,55 @@ -using UnityEngine; - -namespace Unity3dAzure.AppServices -{ - public sealed class ZumoRequest : RestRequest - { - public ZumoRequest (MobileServiceClient client, string url, Method httpMethod) : base (url, httpMethod) - { - this.AddHeader ("ZUMO-API-VERSION", "2.0.0"); - this.AddHeader ("Accept", "application/json"); - this.AddHeader ("Content-Type", "application/json; charset=utf-8"); - - if (client.User != null && !string.IsNullOrEmpty (client.User.authenticationToken)) { - this.AddHeader ("X-ZUMO-AUTH", client.User.authenticationToken); - Debug.Log ("Auth UserId:" + client.User.user.userId); - } - } - - public void AddBodyAccessToken (string token) - { - AccessToken accessToken = new AccessToken (token); - this.AddBody<AccessToken> (accessToken); - } - - public override void AddBody<T> (T data, string contentType = "application/json; charset=utf-8") - { - string jsonString = JsonHelper.ToJsonExcludingSystemProperties (data); // strip App Services System Properties - byte[] bytes = DataHelper.ToUTF8Bytes (jsonString); - this.AddBody (bytes, contentType); - } - - } +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using UnityEngine; +using System.Text; +using RESTClient; + +namespace Azure.AppServices { + public sealed class ZumoRequest : RestRequest { + + // NB: Exclude system properties is a workaround to ignore read-only system properties during JSON utility deserialization. + // If set to true then App Services System Properties will be stripped from the request body during deserialization. + private bool excludeSystemProperties; + + public ZumoRequest(string url, Method httpMethod = Method.GET, bool excludeSystemProperties = true, AuthenticatedUser user = null) : base(url, httpMethod) { + this.excludeSystemProperties = excludeSystemProperties; + this.AddHeader("ZUMO-API-VERSION", "2.0.0"); + this.AddHeader("Accept", "application/json"); + this.AddHeader("Content-Type", "application/json; charset=utf-8"); + // User Authentictated request + if (user != null && !string.IsNullOrEmpty(user.authenticationToken)) { + this.AddHeader("X-ZUMO-AUTH", user.authenticationToken); + } + } + + // Facebook, Microsoft Account, Azure Active Directory + public void AddBodyAccessToken(string token) { + AccessToken accessToken = new AccessToken(token); + this.AddBody<AccessToken>(accessToken); + } + + // Twitter + public void AddBodyAccessTokenSecret(string token, string tokenSecret) { + AccessTokenSecret accessTokenSecret = new AccessTokenSecret(token, tokenSecret); + this.AddBody<AccessTokenSecret>(accessTokenSecret); + } + + // Google+ + public void AddBodyAccessTokenId(string token, string idToken) { + AccessTokenId accessTokenId = new AccessTokenId(token, idToken); + this.AddBody<AccessTokenId>(accessTokenId); + } + + public override void AddBody<T>(T data, string contentType = "application/json; charset=utf-8") { + if (typeof(T) == typeof(string)) { + this.AddBody(data.ToString(), contentType); + return; + } + string jsonString = excludeSystemProperties ? JsonHelper.ToJsonExcludingSystemProperties(data) : JsonUtility.ToJson(data); + byte[] bytes = Encoding.UTF8.GetBytes(jsonString); + this.AddBody(bytes, contentType); + } + + } } diff --git a/model/AccessToken.cs b/model/AccessToken.cs index 411e5d6..65ca563 100644 --- a/model/AccessToken.cs +++ b/model/AccessToken.cs @@ -1,21 +1,16 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Unity3dAzure.AppServices -{ - [Serializable] - public class AccessToken - { - public string access_token; +namespace Azure.AppServices { + [System.Serializable] + public class AccessToken { + public string access_token; - /// <summary> - /// Facebook, Google, AAD access_token request - /// </summary> - public AccessToken (string accessTokenValue) - { - access_token = accessTokenValue; - } - - // Must have a public parameterless contructor in order to use it as paramter 'T' in the generic type or method RestRequest.AddBody<T> - //public AccessToken() {} - } -} \ No newline at end of file + /// <summary> + /// Facebook, Microsoft Account, Azure Active Directory "access_token" request body + /// </summary> + public AccessToken(string accessTokenValue) { + access_token = accessTokenValue.Trim(); + } + } +} diff --git a/model/AccessTokenId.cs b/model/AccessTokenId.cs new file mode 100644 index 0000000..6a9c0df --- /dev/null +++ b/model/AccessTokenId.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + [System.Serializable] + public class AccessTokenId { + public string access_token; + public string id_token; + + /// <summary> + /// Google+ "access_token" and "id_token" request body + /// </summary> + public AccessTokenId(string accessTokenValue, string idTokenValue) { + access_token = accessTokenValue.Trim(); + id_token = idTokenValue.Trim(); + } + } +} diff --git a/model/AccessTokenSecret.cs b/model/AccessTokenSecret.cs new file mode 100644 index 0000000..a171fa7 --- /dev/null +++ b/model/AccessTokenSecret.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + [System.Serializable] + public class AccessTokenSecret { + public string access_token; + public string access_token_secret; + + /// <summary> + /// Twitter with "access_token" and "access_token_secret" request body + /// </summary> + public AccessTokenSecret(string accessTokenValue, string accessTokenSecret) { + access_token = accessTokenValue.Trim(); + access_token_secret = accessTokenSecret.Trim(); + } + } +} diff --git a/model/AuthenticatedUser.cs b/model/AuthenticatedUser.cs new file mode 100644 index 0000000..5a6bb88 --- /dev/null +++ b/model/AuthenticatedUser.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + [System.Serializable] + public class AuthenticatedUser { + public string authenticationToken; + public User user; + } +} diff --git a/model/MobileServiceUser.cs b/model/MobileServiceUser.cs deleted file mode 100644 index a473009..0000000 --- a/model/MobileServiceUser.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Unity3dAzure.AppServices -{ - [Serializable] - public class MobileServiceUser - { - public string authenticationToken; - public User user; - } -} \ No newline at end of file diff --git a/model/ResponseError.cs b/model/ResponseError.cs index f0ba15b..7dcbc5d 100644 --- a/model/ResponseError.cs +++ b/model/ResponseError.cs @@ -1,10 +1,9 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Unity3dAzure.AppServices -{ - [Serializable] - public class ResponseError - { - public string error; - } -} \ No newline at end of file +namespace Azure.AppServices { + [System.Serializable] + public class ResponseError { + public string error; + } +} diff --git a/model/User.cs b/model/User.cs index 479c714..3b7fdcd 100644 --- a/model/User.cs +++ b/model/User.cs @@ -1,10 +1,9 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Unity3dAzure.AppServices -{ - [Serializable] - public class User - { - public string userId; - } -} \ No newline at end of file +namespace Azure.AppServices { + [System.Serializable] + public class User { + public string userId; + } +} diff --git a/model/base/DataModel.cs b/model/base/DataModel.cs index 7f9b82e..16f6f65 100644 --- a/model/base/DataModel.cs +++ b/model/base/DataModel.cs @@ -1,81 +1,74 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; using UnityEngine; -namespace Unity3dAzure.AppServices -{ - [Serializable] - public class DataModel : IDataModel - { - [SerializeField] - public string id; - - public string GetId () - { - return id; - } - - public void SetId (string id) - { - this.id = id; - } - - // System Properties (read only) - [SerializeField] - private string createdAt; - private DateTime? _createdAt; - - public DateTime? CreatedAt () - { - if (_createdAt == null) { - _createdAt = Convert.ToDateTime (createdAt); - } - return _createdAt; - } - - [SerializeField] - private string updatedAt; - private DateTime? _updatedAt; - - public DateTime? UpdatedAt () - { - if (_updatedAt == null) { - _updatedAt = Convert.ToDateTime (updatedAt); - } - return _updatedAt; - } - - [SerializeField] - private string version; - - public string Version () - { - return version; - } - - [SerializeField] - private bool deleted; - - public bool Deleted () - { - return deleted; - } - - [SerializeField] - private string ROW_NUMBER; - - public uint RowNumber () - { - return Convert.ToUInt32 (ROW_NUMBER, 10); - } - - public override string ToString () - { - return JsonUtility.ToJson (this); - } - - public string ToJSON () - { - return JsonHelper.ToJsonExcludingSystemProperties (this); - } - } -} \ No newline at end of file +namespace Azure.AppServices { + [Serializable] + public abstract class DataModel : IDataModel { + [SerializeField] + public string id; + + public string GetId() { + return id; + } + + public void SetId(string id) { + this.id = id; + } + + // System Properties (read only) + [SerializeField] + private string createdAt; + private DateTime? _createdAt; + + public DateTime? CreatedAt() { + if (_createdAt == null) { + _createdAt = Convert.ToDateTime(createdAt); + } + return _createdAt; + } + + [SerializeField] + private string updatedAt; + private DateTime? _updatedAt; + + public DateTime? UpdatedAt() { + if (_updatedAt == null) { + _updatedAt = Convert.ToDateTime(updatedAt); + } + return _updatedAt; + } + + [SerializeField] + private string version; + + public string Version() { + return version; + } + + [SerializeField] + private bool deleted; + + public bool Deleted() { + return deleted; + } + + [SerializeField] + private string ROW_NUMBER; + + public uint RowNumber() { + return Convert.ToUInt32(ROW_NUMBER, 10); + } + + public override string ToString() { + return JsonUtility.ToJson(this); + } + + public string ToJSON() { + return JsonHelper.ToJsonExcludingSystemProperties(this); + } + } +} diff --git a/model/base/IDataModel.cs b/model/base/IDataModel.cs index dbd9c02..b64afd3 100644 --- a/model/base/IDataModel.cs +++ b/model/base/IDataModel.cs @@ -1,7 +1,8 @@ -namespace Unity3dAzure.AppServices -{ - public interface IDataModel - { - string GetId (); - } -} \ No newline at end of file +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + public interface IDataModel { + string GetId(); + } +} diff --git a/model/nested/INestedResults.cs b/model/nested/INestedResults.cs deleted file mode 100644 index 413f49b..0000000 --- a/model/nested/INestedResults.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Unity3dAzure.AppServices -{ - /// <summary> - /// Interface to support table Query with `$inlinecount=allpages` - /// </summary> - public interface INestedResults - { - } - - public interface INestedResults<T> - { - // work-around for WSA - string GetArrayField(); - string GetCountField(); - - void SetArray(T[] array); - void SetCount(uint count); - - } -} \ No newline at end of file diff --git a/model/nested/NestedResults.cs b/model/nested/NestedResults.cs deleted file mode 100644 index 4db78c1..0000000 --- a/model/nested/NestedResults.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace Unity3dAzure.AppServices -{ - /// <summary> - /// Wrap your data model with this object to call the table Query with `$inlinecount=allpages` param. - /// </summary> - [Serializable] - public sealed class NestedResults<T> : INestedResults<T> - { - public uint count; - public T[] results; - - // WSA work-around - public void SetArray(T[] array) - { - this.results = array; - } - - public void SetCount(uint count) - { - this.count = count; - } - - public string GetArrayField() - { - return "results"; - } - - public string GetCountField() - { - return "count"; - } - } -} \ No newline at end of file diff --git a/table/AppServiceTable.cs b/table/AppServiceTable.cs new file mode 100644 index 0000000..304372b --- /dev/null +++ b/table/AppServiceTable.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using UnityEngine; +using System.Collections; +using System; + +namespace Azure.AppServices { + public class AppServiceTable<E> : IAppServiceTable { + private AppServiceClient _client; + private string _name; + + private const string URI_TABLES = "tables"; + + public AppServiceTable(string tableName, AppServiceClient client) { + _client = client; + _name = tableName; // NB: The table name could be infered from the Table's DataModel using typeof(E).Name; but passing Table name as a string allows for the case when the Classname is not the same as the Table name. + } + + public override string ToString() { + return _name; + } + + public IEnumerator Insert<T>(T item, Action<IRestResponse<T>> callback = null) where T : new() { + string url = TableUrl(_name); + ZumoRequest request = new ZumoRequest(url, Method.POST, true, _client.User); + Debug.Log("Insert Request: " + url); + request.AddBody(item); + yield return request.Request.Send(); + request.ParseJson<T>(callback); + } + + public IEnumerator Read<T>(Action<IRestResponse<T[]>> callback = null) where T : new() { + string url = TableUrl(_name); + ZumoRequest request = new ZumoRequest(url, Method.GET, true, _client.User); + Debug.Log("Read Request: " + url); + yield return request.Request.Send(); + request.ParseJsonArray<T>(callback); + } + + public IEnumerator Query<T>(TableQuery query, Action<IRestResponse<T[]>> callback = null) where T : new() { + string url = TableQueryUrl(_name, query.ToString()); + ZumoRequest request = new ZumoRequest(url, Method.GET, true, _client.User); + Debug.Log("Query Request: " + url + " Query:" + query); + yield return request.Request.Send(); + request.ParseJsonArray<T>(callback); + } + + public IEnumerator Query<T>(TableQuery query, Action<IRestResponse<NestedResults<T>>> callback = null) where T : new() { + string q = query.ToString(); + string nestedQueryString = string.Format("{0}{1}$inlinecount=allpages", q, (q.Length > 1) ? "&" : "?"); + string url = TableQueryUrl(_name, nestedQueryString); + Debug.Log("Query Request: " + url + " Paginated Query:" + query); + ZumoRequest request = new ZumoRequest(url, Method.GET, true, _client.User); + yield return request.Request.Send(); + request.ParseJsonNestedArray<T, NestedResults<T>>("results", callback); + } + + public IEnumerator Update<T>(T item, Action<IRestResponse<T>> callback = null) where T : new() { + string id = null; + // Check if item uses the 'IDataModel' Interface to get 'id' property, otherwise try Refelection (using Reflection helper). + IDataModel model = item as IDataModel; + if (model != null) { + id = model.GetId(); + } else if (ReflectionHelper.HasField(item, "id")) { + var x = ReflectionHelper.GetField(item, "id"); + id = x.GetValue(item) as string; + } else { + Debug.LogError("Unable to get 'id' data model property"); + } + if (string.IsNullOrEmpty(id)) { + Debug.LogError("Error 'id' value is missing"); + yield return null; + } + string url = TableUrl(_name, id); + ZumoRequest request = new ZumoRequest(url, Method.PATCH, true, _client.User); + request.AddBody(item); + Debug.Log("Update Request Url: " + url + " patch:" + item); + yield return request.Request.Send(); + request.ParseJson<T>(callback); + } + + public IEnumerator Delete<T>(string id, Action<IRestResponse<T>> callback = null) where T : new() { + string url = TableUrl(_name, id); + ZumoRequest request = new ZumoRequest(url, Method.DELETE, true, _client.User); + Debug.Log("Delete Request Url: " + url); + yield return request.Request.Send(); + request.ParseJson<T>(callback); + } + + public IEnumerator Lookup<T>(string id, Action<IRestResponse<T>> callback = null) where T : new() { + string url = TableUrl(_name, id); + ZumoRequest request = new ZumoRequest(url, Method.GET, true, _client.User); + Debug.Log("Lookup Request Url: " + url); + yield return request.Request.Send(); + request.ParseJson<T>(callback); + } + + private string TableUrl(string tableName, string path = "") { + string resourcePath = string.IsNullOrEmpty(path) ? tableName : tableName + "/" + path; + return string.Format("{0}/{1}/{2}", _client.Url, URI_TABLES, resourcePath); + } + + private string TableQueryUrl(string tableName, string query) { + return string.Format("{0}/{1}/{2}{3}", _client.Url, URI_TABLES, tableName, query); + } + + } +} diff --git a/table/IAppServiceTable.cs b/table/IAppServiceTable.cs new file mode 100644 index 0000000..2841d7d --- /dev/null +++ b/table/IAppServiceTable.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; +using System.Collections; + +namespace Azure.AppServices { + public interface IAppServiceTable { + /// <summary> + /// Create a new item. + /// </summary> + IEnumerator Insert<T>(T item, Action<IRestResponse<T>> callback = null) where T : new(); + + /// <summary> + /// Get a list of items. + /// </summary> + IEnumerator Read<T>(Action<IRestResponse<T[]>> callback = null) where T : new(); + + /// <summary> + /// Update an item's data using id property. + /// </summary> + IEnumerator Update<T>(T item, Action<IRestResponse<T>> callback = null) where T : new(); + + /// <summary> + /// Delete an item using id property. + /// </summary> + IEnumerator Delete<T>(string id, Action<IRestResponse<T>> callback = null) where T : new(); + + /// <summary> + /// Get a list of results using an App Service table query. + /// More info about queries here: + /// https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx + /// </summary> + IEnumerator Query<T>(TableQuery query, Action<IRestResponse<T[]>> callback = null) where T : new(); + + /// <summary> + /// Returns a 'count' and nested list of 'results' (appends `$inlinecount=allpages` parameter to the query) + /// </summary> + IEnumerator Query<T>(TableQuery query, Action<IRestResponse<NestedResults<T>>> callback = null) where T : new(); + + /// <summary> + /// Get an item's data using id property. + /// </summary> + IEnumerator Lookup<T>(string id, Action<IRestResponse<T>> callback = null) where T : new(); + } +} diff --git a/table/IAzureMobileServiceTable.cs b/table/IAzureMobileServiceTable.cs deleted file mode 100644 index f3b70a7..0000000 --- a/table/IAzureMobileServiceTable.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections; - -namespace Unity3dAzure.AppServices -{ - public interface IAzureMobileServiceTable - { - /// <summary> - /// Create a new item. - /// </summary> - IEnumerator Insert<T> (T item, Action<IRestResponse<T>> callback = null) where T : new(); - - /// <summary> - /// Get a list of items. - /// </summary> - IEnumerator Read<T> (Action<IRestResponse<T[]>> callback = null) where T : new(); - - /// <summary> - /// Update an item's data using id property. - /// </summary> - IEnumerator Update<T> (T item, Action<IRestResponse<T>> callback = null) where T : new(); - - /// <summary> - /// Delete an item using id property. - /// </summary> - IEnumerator Delete<T> (string id, Action<IRestResponse<T>> callback = null) where T : new(); - - /// <summary> - /// Get a list of results using a custom query. - /// More info about custom queries here: - /// https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx - /// </summary> - IEnumerator Query<T> (CustomQuery query, Action<IRestResponse<T[]>> callback = null) where T : new(); - - /// <summary> - /// Returns a 'count' and nested list of 'results' (appends `$inlinecount=allpages` parameter to the query) - /// </summary> - IEnumerator Query<T>(CustomQuery query, Action<IRestResponse<NestedResults<T>>> callback = null) where T : new(); - - /// <summary> - /// Get an item's data using id property. - /// </summary> - IEnumerator Lookup<T> (string id, Action<IRestResponse<T>> callback = null) where T : new(); - } -} \ No newline at end of file diff --git a/table/MobileServiceTable.cs b/table/MobileServiceTable.cs deleted file mode 100644 index c5232e2..0000000 --- a/table/MobileServiceTable.cs +++ /dev/null @@ -1,121 +0,0 @@ -using UnityEngine; -using System.Collections; -using System; - -namespace Unity3dAzure.AppServices -{ - public class MobileServiceTable<E> : IAzureMobileServiceTable - { - private MobileServiceClient _client; - private string _name; - - public const string URI_TABLES = "tables/"; - - public MobileServiceTable (string tableName, MobileServiceClient client) - { - _client = client; - _name = tableName; // NB: The table name could be infered from the Table's DataModel using typeof(E).Name; but passing Table name as a string allows for the case when the Classname is not the same as the Table name. - } - - public override string ToString () - { - return _name; - } - - public IEnumerator Insert<T> (T item, Action<IRestResponse<T>> callback = null) where T : new() - { - string url = string.Format ("{0}/{1}{2}", _client.AppUrl, URI_TABLES, _name); - ZumoRequest request = new ZumoRequest (_client, url, Method.POST); - Debug.Log ("Insert Request: " + url); - request.AddBody (item); - yield return request.request.Send (); - request.ParseJson<T> (callback); - } - - public IEnumerator Read<T> (Action<IRestResponse<T[]>> callback = null) where T : new() - { - string url = string.Format ("{0}/{1}{2}", _client.AppUrl, URI_TABLES, _name); - ZumoRequest request = new ZumoRequest (_client, url, Method.GET); - Debug.Log ("Read Request: " + url); - yield return request.request.Send (); - request.ParseJsonArray<T> (callback); - } - - public IEnumerator Query<T> (CustomQuery query, Action<IRestResponse<T[]>> callback = null) where T : new() - { - string url = string.Format ("{0}/{1}{2}{3}", _client.AppUrl, URI_TABLES, _name, query); - ZumoRequest request = new ZumoRequest (_client, url, Method.GET); - Debug.Log ("Query Request: " + url + " Query:" + query); - yield return request.request.Send (); - request.ParseJsonArray<T> (callback); - } - /* - public IEnumerator Query<T,N> (CustomQuery query, Action<IRestResponse<N>> callback = null) where N : INestedResults<T> where T : new() - { - string queryString = query.ToString (); - string q = queryString.Length > 0 ? "&" : "?"; - queryString += string.Format ("{0}$inlinecount=allpages", q); - string url = string.Format ("{0}/{1}{2}{3}", _client.AppUrl, URI_TABLES, _name, queryString); - Debug.Log ("Query Request: " + url + " Paginated Query:" + query); - ZumoRequest request = new ZumoRequest (_client, url, Method.GET); - yield return request.request.Send (); - request.TryParseJsonNestedArray<T,N> ("results", callback); - } - */ - public IEnumerator Query<T>(CustomQuery query, Action<IRestResponse<NestedResults<T>>> callback = null) where T : new() - { - string queryString = query.ToString(); - string q = queryString.Length > 0 ? "&" : "?"; - queryString += string.Format("{0}$inlinecount=allpages", q); - string url = string.Format("{0}/{1}{2}{3}", _client.AppUrl, URI_TABLES, _name, queryString); - Debug.Log("Query Request: " + url + " Paginated Query:" + query); - ZumoRequest request = new ZumoRequest(_client, url, Method.GET); - yield return request.request.Send(); - request.ParseJsonNestedArray<T, NestedResults<T>>("results", callback); - } - - public IEnumerator Update<T> (T item, Action<IRestResponse<T>> callback = null) where T : new() - { - string id = null; - // Check if model uses the 'IDataModel' Interface to get id property, otherwise try Refelection (using 'Model' helper). - IDataModel model = item as IDataModel; - if (model != null) { - id = model.GetId (); - } else if (Model.HasField (item, "id")) { - var x = Model.GetField (item, "id"); - id = x.GetValue (item) as string; - } else { - Debug.LogError ("Unable to get 'id' data model property"); - } - if (string.IsNullOrEmpty (id)) { - Debug.LogError ("Error 'id' value is missing"); - yield return null; - } - string url = string.Format ("{0}/{1}{2}/{3}", _client.AppUrl, URI_TABLES, _name, id); - ZumoRequest request = new ZumoRequest (_client, url, Method.PATCH); - request.AddBody (item); - Debug.Log ("Update Request Url: " + url + " patch:" + item); - yield return request.request.Send (); - request.ParseJson<T> (callback); - } - - public IEnumerator Delete<T> (string id, Action<IRestResponse<T>> callback = null) where T : new() - { - string url = string.Format ("{0}/{1}{2}/{3}", _client.AppUrl, URI_TABLES, _name, id); - ZumoRequest request = new ZumoRequest (_client, url, Method.DELETE); - Debug.Log ("Delete Request Url: " + url); - yield return request.request.Send (); - request.ParseJson<T> (callback); - } - - public IEnumerator Lookup<T> (string id, Action<IRestResponse<T>> callback = null) where T : new() - { - string url = string.Format ("{0}/{1}{2}/{3}", _client.AppUrl, URI_TABLES, _name, id); - ZumoRequest request = new ZumoRequest (_client, url, Method.GET); - Debug.Log ("Lookup Request Url: " + url); - yield return request.request.Send (); - request.ParseJson<T> (callback); - } - - } -} \ No newline at end of file diff --git a/table/query/CustomQuery.cs b/table/query/CustomQuery.cs deleted file mode 100644 index e1bf8cd..0000000 --- a/table/query/CustomQuery.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Text; -using UnityEngine; - -/// <summary> -/// Implemention of Query records operation https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx -/// There is a maximum of 50 records returned in a query - use top and skip params to return additional pages of results. -/// NB: `$inlinecount` (which returns count of all items without paging applied) is not set here as it changes the data model shape and the way the REST decode callback works makes it non-trival to decode. -/// Rather the '$inlinecount=allpages' param is automatically set when using the table's Query method and wrapping your data model with the NestedResults object wrapper. -/// </summary> -namespace Unity3dAzure.AppServices -{ - [Flags] - public enum MobileServiceSystemProperty - { - nil = 0x0, - createdAt = 0x1, - updatedAt = 0x2, - version = 0x4, - deleted = 0x8 - } - - public class CustomQuery - { - // query option parameters defined by the Open Data Protocol (OData) - public string Filter; - public string OrderBy; - public uint Top; - public uint Skip; - public string Select; - // other params - public MobileServiceSystemProperty SystemProperties; - public bool IncludeDeleted; - - public CustomQuery (string filter = "", string orderBy = null, uint top = 0, uint skip = 0, string select = null, MobileServiceSystemProperty systemProperties = MobileServiceSystemProperty.nil, bool includeDeleted = false) - { - this.Filter = filter; // return only rows that satisfy the specified filter predicate - this.OrderBy = orderBy; // sort column by one or more columns: order can be specified in 'desc' or 'asc' order ('asc' is default) - this.Top = top; // return the top n entities for any query - this.Skip = skip; // the n of records to skip (used for paging results) - this.Select = select; // defines new projection of data by specifying the columns - this.SystemProperties = systemProperties; // list of system properties to be included in the response - this.IncludeDeleted = includeDeleted; // if table has soft delete enabled then deleted records will be included in the results - } - - public static CustomQuery CreateWithOrderBy (string orderBy) - { - return new CustomQuery ("", orderBy); - } - - public override string ToString () - { - string queryString = ""; - string q = "?"; - if (!string.IsNullOrEmpty (this.Filter)) { - queryString += string.Format ("{0}$filter=({1})", q, this.Filter); - q = "&"; - } - if (!string.IsNullOrEmpty (this.OrderBy)) { - queryString += string.Format ("{0}$orderby={1}", q, this.OrderBy); - q = "&"; - } - if (this.Top > 0) { - queryString += string.Format ("{0}$top={1}", q, this.Top.ToString ()); - q = "&"; - } - if (this.Skip > 0) { - queryString += string.Format ("{0}$skip={1}", q, this.Skip.ToString ()); - q = "&"; - } - if (!string.IsNullOrEmpty (this.Select)) { - queryString += string.Format ("{0}$select={1}", q, this.Select); - q = "&"; - } - if (this.SystemProperties != MobileServiceSystemProperty.nil) { - // NB: setting __systemproperties param doesn't seem to do anything different as these properties are all included by default, but we can append values to the 'select' param. - if (!string.IsNullOrEmpty (this.Select)) { - queryString += string.Format (",{0}", SystemPropertiesValues (this.SystemProperties)); - } - queryString += string.Format ("{0}__systemproperties={1}", q, SystemPropertiesValues (this.SystemProperties)); - q = "&"; - } - if (this.IncludeDeleted) { - queryString += string.Format ("{0}__includeDeleted=true", q); - } - return EscapeURL (queryString); - } - - private string EscapeURL (string query) - { - string q = WWW.EscapeURL (query); - StringBuilder sb = new StringBuilder (q); - sb.Replace ("+", "%20"); // NB: replace space with '%20' instead of '+' - // keep query params: ?&=$ - sb.Replace ("%3f", "?"); - sb.Replace ("%26", "&"); - sb.Replace ("%3d", "="); - sb.Replace ("%24", "$"); - return sb.ToString (); - } - - private string SystemPropertiesValues (MobileServiceSystemProperty systemProperties) - { - if (systemProperties == MobileServiceSystemProperty.nil) { - return ""; - } - return systemProperties.ToString ().Replace (" ", ""); // remove spaces from string - } - } -} \ No newline at end of file diff --git a/table/query/OrderBy.cs b/table/query/OrderBy.cs new file mode 100644 index 0000000..1ea0bcf --- /dev/null +++ b/table/query/OrderBy.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using UnityEngine; + +namespace Azure.AppServices { + + public enum SortDirection { + asc, + desc + } + + public class OrderBy { + private string column; + private SortDirection? columnSortDirection; + + public OrderBy(string column, SortDirection? sortDirection = null) { + this.column = WWW.EscapeURL(column); + this.columnSortDirection = sortDirection; + } + + public override string ToString() { + if (!string.IsNullOrEmpty(column)) { + string sort = (columnSortDirection != null) ? "+" + columnSortDirection : ""; + return column + sort; + } + return ""; + } + + } +} diff --git a/table/query/TableQuery.cs b/table/query/TableQuery.cs new file mode 100644 index 0000000..5775154 --- /dev/null +++ b/table/query/TableQuery.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using UnityEngine; + +/// <summary> +/// Implemention of Query records operation https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx +/// By default there is a maximum of 50 records returned in a query - use top and skip params to return additional pages of results. +/// NB: The `$inlinecount` param (which returns total count of all items without paging applied) is not set here as it alters the data model shape. +/// Rather the '$inlinecount=allpages' param is automatically set when using the table's Query method and wrapping your data model with the NestedResults object. +/// </summary> +namespace Azure.AppServices { + [Flags] + public enum TableSystemProperty { + nil = 0x0, + createdAt = 0x1, + updatedAt = 0x2, + version = 0x4, + deleted = 0x8 + } + + public class TableQuery { + public string Filter; + public OrderBy[] OrderBy; + public string Select; + public uint Skip; + public uint Top; + + // other params + public bool IncludeDeleted; + public TableSystemProperty SystemProperties; + public bool NoScript = false; // When a value of `true` is supplied, the execution of registered scripts is suppressed. + // NB: To suppress script execution, you must also supply the service master key with the "X-ZUMO-MASTER" header. + // For more info and security note refer to: https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx + + public TableQuery(string filterPredicate = "", uint top = 0, uint skip = 0, string selectColumns = null, TableSystemProperty systemProperties = TableSystemProperty.nil, bool includeDeleted = false, params OrderBy[] orderBy) { + // supports the following subset of the query option parameters defined by the Open Data Protocol (OData) + this.Filter = filterPredicate; // return only rows that satisfy the specified filter predicate + this.OrderBy = orderBy; // sort column by one or more columns: order can be specified in 'desc' or 'asc' order ('asc' is default) + this.Select = selectColumns; // defines new projection of data by specifying the columns + this.Skip = skip; // the n of records to skip (used for paging results) + this.Top = top; // return the top n entities for any query + + this.IncludeDeleted = includeDeleted; // if table has soft delete enabled then deleted records will be included in the results + this.SystemProperties = systemProperties; // list of system properties to be included in the response + } + + public static TableQuery CreateWithOrderBy(params OrderBy[] orderBy) { + return new TableQuery("", 0, 0, null, TableSystemProperty.nil, false, orderBy); + } + + public override string ToString() { + StringBuilder sb = new StringBuilder("?"); + if (!string.IsNullOrEmpty(Filter)) { + sb.Append(string.Format("$filter=({0})&", WWW.EscapeURL(Filter))); + } + if (OrderBy != null && OrderBy.Length > 0) { + sb.Append(string.Format("$orderby=")); + foreach (OrderBy order in OrderBy) { + sb.Append(order.ToString() + ","); + } + sb.Remove(sb.Length - 1, 1); // remove trailing ',' + sb.Append(string.Format("&")); + } + if (Top > 0) { + sb.Append(string.Format("$top={0}&", Top.ToString())); + } + if (Skip > 0) { + sb.Append(string.Format("$skip={0}&", Skip.ToString())); + } + if (!string.IsNullOrEmpty(Select)) { + sb.Append(string.Format("$select={0}&", WWW.EscapeURL(Select))); + if (SystemProperties != TableSystemProperty.nil) { + // NB: setting `__systemproperties` param doesn't appear to do anything so I've added a workaround to append these values onto the 'select' param. + sb.Remove(sb.Length - 1, 1); // remove trailing '&' + sb.Append(string.Format(",{0}&", SystemPropertiesValues(this.SystemProperties))); + } + } + if (SystemProperties != TableSystemProperty.nil) { + sb.Append(string.Format("__systemproperties={0}&", SystemPropertiesValues(SystemProperties))); + } + if (this.IncludeDeleted) { + sb.Append("__includeDeleted=true&"); + } + if (this.NoScript) { + sb.Append("noscript=true&"); + } + sb.Remove(sb.Length - 1, 1); // remove trailing '&' + return sb.ToString(); + } + + private string SystemPropertiesValues(TableSystemProperty systemProperties) { + if (systemProperties == TableSystemProperty.nil) { + return ""; + } + return systemProperties.ToString().Replace(" ", ""); // remove spaces from string + } + } +}