Welcome

Welcome to Backendless, a powerful API services platform. The platform can be used to build and run mobile and web applications. Backendless consists of built-in, general purpose API services and also supports deployment of custom, developer-defined services and functions. The built-in API services support common backend tasks and operations such as user login and logout, password recovery, data persistence, file upload, etc. Developer-defined services and functions can be used for custom server-side business logic. The diagram below illustrates major components of the Backendless architecture:

backendless-architecture

The sections below describe individual service APIs. It is recommended to go through the Setup section first to configure your environment.

Client-side Setup

To start using the Backendless APIs, .NET and Xamarin applications must add a reference for the Backendless library and  its dependencies. The library is available as a NuGet package.

 

Once the Backendless reference is added, the client application must initialize the library with the code shown below. The code must be executed prior to any API calls:

Backendless.InitApp( application-Id, dotNET-API-key );

Most of the Backendless SDK for .NET classes are available in the BackendlessAPI and BackendlessAPI.Async namespaces. Import the classes with the following statements:

using BackendlessAPI;
using BackendlessAPI.Async;
using BackendlessAPI.Exception;

Application ID and API Key

Values for the application-id and api-key headers must be obtained through the Backendless Console:

1. Login to your account and select the application.
2. Click the Manage icon from the vertical icon-menu on the left.
3. The App Settings section is selected by default. The interface contains values for Application ID and API keys for each supported client-side environment.
4. Use the Copy icon to copy the value into the system clipboard.
appsettings-keys.zoom60

Make sure to use the ".NET API Key" for the dotNET-API-key argument.

Blocking and Non-blocking APIs

Most of Backendless APIs for .NET are available as synchronous (blocking) and asynchronous (non-blocking) implementations. The synchronous (blocking APIs) block the execution of the thread where they are called until a result is received from the server. The asynchronous (non-blocking) APIs do block, the program can continue its execution.

 

The asynchronous (non-blocking) APIs are available in two formats:

1. Methods with a callback argument. The argument contains two delegate methods, one to receive the result for a successful API invocation and another to receive an error if it is returned by the server. The API methods with the callback argument always have AsyncCallback<T> in the method declaration. Usually it is the last argument in a signature:
Backendless.UserService.Register( user, new AsyncCallback<BackendlessUser>( 
    registeredUser =>
    {
      Console.WriteLine( $"User has been registered. " + 
                         $"User's object ID - {registeredUser.ObjectId}" );   
    },
    error =>
    {
       Console.WriteLine( $"Server reported an error - {error.Message}"); 
    }));
2. Methods marked with the async keyword. The return type of these methods is either System.Threading.Tasks.Task or System.Threading.Tasks.Task<some-type> and the method names always end with Async. Since the async and await keywords were introduced in .NET 4.5, these methods are available in the target frameworks 4.5 and above.
Task<BackendlessUser> task = Backendless.UserService.RegisterAsync( user );

 

For the first format of the asynchronous APIs (the ones with the AsyncCallback<T> argument), the class has the following declaration:

using BackendlessAPI.Exception;

namespace BackendlessAPI.Async
{
  public delegate void ErrorHandler( BackendlessFault _backendlessFault );
  public delegate void ResponseHandler<T>( T response );

  public class AsyncCallback<T>
  {
    internal ErrorHandler ErrorHandler;
    internal ResponseHandler<T> ResponseHandler;

    public AsyncCallback( ResponseHandler<T> responseHandler, ErrorHandler errorHandler )
    {
      this.ResponseHandler = responseHandler;
      this.ErrorHandler = errorHandler;
    }
  }
}

where the ResponseHandler<T>( T response )delegate is called when a response for an asynchronous operation is available. If operation results in an error, it is delivered to the ErrorHandler delegate. See the Error Handling section for additional information on how to process errors.

 

Error Handling

When the server reports an error, it is delivered to the client through an instance of the BackendlessFault class. A BackendlessFault object provides access to an error code which uniquely identifies the root cause of the error. In addition to the error code, the fault object may include an error message which provides additional information about the error:

namespace BackendlessAPI.Exception
{
  public class BackendlessFault
  {
    // error code
    public string FaultCode;
    
    // error message
    public string Message;
    
    // error details;
    public string Detail; 
  }
}

The asynchronous API calls accept the AsyncCallback argument which receives the fault object through the following delegate:

public delegate void ErrorHandler( BackendlessFault fault );

 

For the APIs which use the async keyword, errors are thrown as exceptions. The top exception is System.AggregateException which wraps the underlying exception containing the information about the error.

 

If a server error occurs during a synchronous/blocking API invocation, it is thrown to the client as a checked exception - BackendlessException:

namespace BackendlessAPI.Exception
{
  public class BackendlessException : System.Exception
  {
    public BackendlessFault BackendlessFault { get; }
    public string FaultCode { get; }
    public string Detail { get; }
    public override string Message { get; }
    public override string ToString();
  }
}


Error Codes

Click here for the complete list of Backendless Error Codes

User Service API

Overview

The Backendless User Service provides the functionality related to the user accounts such as user registrations, logins, password recovery and logouts. The core concept which the User Service relies on is the User entity. The structure of the entity is configurable, that is a developer can decide which properties "describe" a user in the context of the application. Typically, properties describing a user are the ones collected during the user registration process.  The User Service provides the API enabling the following functionality for the applications built on top of Backendless:

 

User Registration - Applications use the Backendless' registration API to let the users register and create accounts for subsequent logins. Application developers can restrict access to the server-side resources for specific user accounts or based on roles.
User Login - the API lets the registered users login to establish their identity within the application.
Password Recovery - Backendless supports a complete workflow allowing users to recover lost or forgotten passwords.
User Logout - the API lets the logged in users terminate their session and disassociate their identity from the application.
Updating User Registration - the API supports the operation of updating user information.

Retrieve User Schema

An application can get a list of the properties associated with the user schema by using the following API:

Non-blocking Method:

public void Backendless.UserService.DescribeUserClass( AsyncCallback<List<UserProperty>> callback );

where:

callback - an object which receives either a return value or an error from the server. The return value from the server is a collection of the UserProperty objects.

Blocking Method:

public List<UserProperty> Backendless.UserService.DescribeUserClass();

The UserProperty class defines the following properties:

// returns true if the property is marked as 'identity'
public bool IsIdentity { get;  }
    
// returns the name of the property
public string Name  { get; }
    
// returns true if the property is required during user registration
public bool IsRequired  { get;  }
    
// returns the data type of the property
public DateTypeEnum Type  { get;  }
    
// returns the default value which is assigned to the property
// when it is missing during the user registration operation
public object DefaultValue  { get; }

// returns true if the property/column represents a relation
// and is marked as auto-load
public bool AutoLoad( get; } 

// Returns the validator regular expression, if the 
// property/column has a validator assigned to it.
public string CustomRegex { get; }

// Returns the name of the related table, if the
// property/column represents a relation
public string RelatedTable { get; }

Non-blocking Method Example:

using BackendlessAPI;
using BackendlessAPI.Async;
using BackendlessAPI.Property;

AsyncCallback<List<UserProperty>> callback;
callback = new AsyncCallback<List<UserProperty>>(
        props =>
        {
          foreach( UserProperty p in props )
          {
            System.Console.WriteLine( "prop name " + p.Name );
            System.Console.WriteLine( "\tis identity " + p.IsIdentity );
            System.Console.WriteLine( "\tis required " + p.IsRequired );
            System.Console.WriteLine( "\tprop type " + p.Type );
            System.Console.WriteLine( "\tdefault value " + p.DefaultValue );
          }
        },
        fault =>
        {
          System.Console.WriteLine( fault.ToString() );
        } );

Backendless.UserService.DescribeUserClass( callback );

Blocking Method Example:

using BackendlessAPI;
using BackendlessAPI.Async;
using BackendlessAPI.Property;

List<UserProperty> properties = Backendless.UserService.DescribeUserClass();

foreach( UserProperty p in properties )
{
  System.Console.WriteLine( "prop name " + p.Name );
  System.Console.WriteLine( "\tis identity " + p.IsIdentity );
  System.Console.WriteLine( "\tis required " + p.IsRequired );
  System.Console.WriteLine( "\tprop type " + p.Type );
  System.Console.WriteLine( "\tdefault value " + p.DefaultValue );
}

 

User Registration

The user registration API can be used to create user accounts in the application. A registration request must provide a user object as a collection of key/value properties. The collection must contain all the required properties which must include a property marked as identity as well as the "password" property. The default identity property is "email", however, it can be changed in Backendless Console. Additionally, the "email" property is required if the application is configured to confirm email addresses for the registered users.

Non-blocking Method:

Backendless.UserService.Register( BackendlessUser user, 
                                  AsyncCallback<BackendlessUser>() callback );

Blocking Method:

public BackendlessUser Backendless.UserService.Register( BackendlessUser user );

where:

user - an instance of the BackendlessUser class which contains property values for the account registration.
callback - an object which receives either a return value or an error from the server. The return value from the server is an instance of the BackendlessUser class with the ID assigned by the server-side.

Return value:

An instance of BackendlessUser representing registered user. The objectId property contains a value assigned by the server. The registration API does not login the user.

Error Codes:

The following errors may occur during User Registration API calls. See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Invalid application info (application id or api key)

3009

User registration is disabled for the application

3010

User registration has an unknown property and dynamic properties are disabled for this version of the application

3011

Missing "password" property

3012

Required property is missing

3013

Missing value for the identity property

3014

External registration failed with an error.

3021

General user registration error. Details included with the error message.

3033

User with the same identity already exists

3038

Missing application-id or collection of properties for the registering user

3039

Property "id" cannot be used in the registration call

3040

Email address is in the wrong format

3041

A value for a required property is missing

3043

Duplicate properties in the registration request

8000

Property value exceeds the length limit

 

Non-blocking Example:

// do not forget to call Backendless.App.Init in the app initialization code
AsyncCallback<BackendlessUser> callback = new AsyncCallback<BackendlessUser>(
       user =>
        {
          System.Console.WriteLine( "User registered. Assigned ID - " + user.ObjectId );
          Dictionary<string, object> props = user.Properties;

          foreach( KeyValuePair<string, object> pair in props )
            System.Console.WriteLine( String.Format( "Property: {0} - {1}", pair.Key, pair.Value ) );
        },
        fault =>
        {
          System.Console.WriteLine( fault.ToString() );
        } );

BackendlessUser newUser = new BackendlessUser();
newUser.SetProperty( "login", "james.bond" );
newUser.SetProperty( "email", "jb@mi6.co.uk" );
newUser.Password = "hard2guess";
Backendless.UserService.Register( newUser, callback );

Blocking Example:

do not forget to call Backendless.App.Init in the app initialization code

BackendlessUser newUser = new BackendlessUser();
newUser.SetProperty( "login", "james.bond" );
newUser.SetProperty( "email", "jb@mi6.co.uk" );
newUser.Password = "hard2guess";
newUser = Backendless.UserService.Register( newUser );

System.Console.WriteLine( "User registered. Assigned ID - " + user.Id );
Dictionary<string, object> props = user.Properties;

foreach( KeyValuePair<string, object> pair in props )
  System.Console.WriteLine( String.Format( "Property: {0} - {1}", pair.Key, pair.Value ) );

Turning Registration Off

User registration can be disabled for a particular version of the application using the Backendless Console:

1. Login to the console and select the application.
2. Click the Users icon in the vertical icon menu on the left.
3. Click User Registration.

The Registration toggle turns the registration API on or off. When the registration is turned off and a user attempts to register, the system returns error 3009.

registration-togglev4.zoom70

Email Confirmations

Backendless can send out an email requesting new registered users to confirm their email address. This feature can be configured in the Backendless Console:

1. Log into the console and select the application.
2. Click the Users icon in the vertical icon menu on the left.
3. Click User Registration.

email-confirmationv4.zoom70

When email confirmations are turned on (the feature is disabled by default), the "email" user property is required and must contain a value formatted as an email address. To configure the text of the email message, select Email Templates from the Users menu in the console and select the User made registration event.

Login

Registered users can login to establish their identity with the application using the API below. The login operation requires two properties: one marked as user identity and the second is password. Backendless automatically assigns the "AuthenticatedUser" role to all successfully logged in users. The role can be used to differentiate access to various resources (persistent objects, messaging channels, files) between authenticated users and guests.

Non-blocking Method:

public void Backendless.UserService.Login( String login, 
                                            String password, 
                                            AsyncCallback<BackendlessUser> callback );
                                            
public void Backendless.UserService.Login( String login, 
                                           String password, 
                                           AsyncCallback<BackendlessUser> callback, 
                                           bool stayLoggedIn )

Blocking Method:

public BackendlessUser Backendless.UserService.login( String login, 
                                                      String password );

public BackendlessUser Backendless.UserService.Login( String login, 
                                                      String password, 
                                                      bool stayLoggedIn )

where:

login - a value for the property marked as identity.
password - user's password
callback - an object which receives either a return value or an error from the server.
stayLoggedIn - requests to store the user's login information so the login form can be skipped next time the user launches the app.

Return value:

a BackendlessUser object representing the logged in user. The object has the values for all the properties stored in Backendless database including those for which the "auto-load" option is enabled.

Errors:

The following errors may occur during the Login API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Version is disabled or provided wrong application info (application id or secret key)

3000

Login has been disabled for the user account.

3001

Missing login settings, possibly invalid application id or version.

3002

User cannot login because Multiple Logins disabled and there is a logged in user for the account.

3003

Invalid login or password.

3006

Either login or password is an empty string value.

3034

User logins are disabled for the version of the application.

3036

Account locked out due to too many failed logins.

3038

One of the required parameters (application id, version, login or password) is null

3044

Multiple login limit for the same user account has been reached.

8000

Property value exceeds the length limit

 

Non-blocking Example:

// do not forget to call Backendless.App.Init in the app initialization code
 
AsyncCallback<BackendlessUser> callback = new AsyncCallback<BackendlessUser>(
        user =>
        {
          System.Console.WriteLine( "User logged in. Assigned ID - " + user.ObjectId );
          Dictionary<string, object> props = user.Properties;
          foreach( KeyValuePair<string, object> pair in props )
            System.Console.WriteLine( String.Format( "Property: {0} - {1}", pair.Key, pair.Value ) );
        },
        fault =>
        {
          System.Console.WriteLine( fault.ToString() );
        } );

String login = "james.bond123";
String password = "guessIt";
Backendless.UserService.Login( login, password, callback );

Blocking Example:

// do not forget to call Backendless.InitApp in the app initialization code

BackendlessUser user;

try
{
  String login = "james.bond123";
  String password = "guessIt";
  user = Backendless.UserService.Login( login, password );
  System.Console.WriteLine( "User is logged in. ObjectId - " + user.ObjectId );
}
catch( BackendlessException exception )
{
  // login failed, to get the error code, use exception.Fault.FaultCode
}

Validating User Login

The login operation provides a way to persist the user-token on the client side so it can be used when the application is restarted. This helps in streamlining the user experience since the user of the application does not need to login again. However, when the application restarts, it needs to check if the underlying user token, and hence the user session are still valid. This can be accomplished with the API below:

 

Non-blocking Method:

public void Backendless.UserService.IsValidLogin( AsyncCallback<Boolean> callback );

Blocking Method:

public bool Backendless.UserService.IsValidLogin()

If the login (user token) is valid, objectId of the logged in user can be retrieved with the following call. The method returns null if login has not been previously persisted:

string loggedInUserObjectId = Backendless.UserService.LoggedInUserObjectId();

Subsequently, the BackendessUser instance can be obtained with the following API:

Non-blocking call:

Backendless.Data.Of<BackendlessUser>().FindById( 
                            loggedInUserObjectId, 
                            AsyncCallback<BackendlessUser> callback )

Blocking call:

Backendless.Data.Of<BackendlessUser>).FindById( loggedInUserObjectId )

Example:

Log in a user first. Make sure the stayLoggedIn argument is true. The value of true persists the login information:

Backendless.UserService.Login( "batman@backendless.com", "superm@n", true );

Then, check whether the login is valid - see the example below:

Blocking example:

Boolean isValidLogin = Backendless.UserService.IsValidLogin();
System.Console.WriteLine( "Is login valid? - " + isValidLogin );

Non-blocking example:

AsyncCallback<Boolean> callback = new AsyncCallback<Boolean>(
  isValidLogin =>
  {
    System.Console.WriteLine( "Is login valid? - " + isValidLogin );
  },
  fault =>
  {
    System.Console.WriteLine( fault.ToString() );
  } );

Backendless.UserService.IsValidLogin( callback );

User Properties

When a user registers with an application, he provides information which establishes user's identity. For one application these properties may include name, phone number and email address, for another it could be user id, age and gender. Backendless User Service allows each application to have a custom set of properties associated with the user entity.

Defining properties with the API

User properties must be defined in the object representing a user. When user registers or user object is updated, Backendless saves the property values in the Users table of Backendless Data service. If a property is not declared on the server-side, Backendless automatically creates a definition for it. This behavior can be turned off/on using the Dynamic Schema Definition configuration setting in the console (select Data > Configuration). The setting is turned on by default:

dynamic-schema-definition.zoom70

 

Set/Update User Property

When setting or updating a user property, the values must be of "primitive" data types (boolean, string, number, date). To assign a non-primitive value, use the Data relation API.

The following API can be used to set or update properties in a user object:

Methods to set/add user properties are available in the BackendlessUser class. When registering a new user, the methods below can be used on a new instance of BackendlessUser. When updating an existing user, the instance must be retrieved from the server (either via login or data object retrieval APIs).

// adding a new property
public void AddProperty( string key, object value )

// changing value for an existing property
public void SetProperty( string key, object value )

// if a property from "newProps" exists in BackendlessUser object,
// it is updated, otherwise, it is added to the object.
public void PutProperties( Dictionary<string, object> newProps )

Retrieve User Property Values

The following API can be used to get user property values:

// retrieves property value for the specified name identified by the "key" argument
public object GetProperty( string key )

// get a complete collection of the user object properties
public Dictionary<string, object> Properties;

Example:

The example below describes how to retrieve the phoneNumber user property.

BackendlessUser user = // user object retrieval is out of scope of the example

// get the value of the "phoneNumber" property
String phoneNumber = (String) user.Properties[ "phoneNumber" ];

// alternatively, the property can be obtained as:
String phoneNumber2 = (String) user.GetProperty( "phoneNumber" );

Defining properties with Console

User property management is available in the schema editor of the Users table. To open the schema editor:

1. Login to Backendless Console and select your application.
2. Click the Data icon, and select the Users table.
3. Click the Schema menu.

There are built-in user properties, such as email, name and password. The email and name properties can be deleted if they do not apply to your app, although it is recommended to keep the email property, since it is used for other purposes (for instance, password recovery). The password property cannot be deleted.

Identity Property

It is required that one of the properties is marked as identity. This is the property Backendless uses for the Login and Restore Password operations. As users register, Backendless ensures the value for the identity property is unique  in the context of a specific version of the application.

Password Property

"password" is a special property. Backendless automatically adds the property when an application is created. The following rules apply to the password property:

Password cannot be removed from the application.
Password cannot be used as identity.
Password is always a required property

users-schema.zoom80

To add a new property, click the New button.

Update User Object

User property values of the logged in users can be updated with an API operation described below. This operation is useful if an application needs to provide to the users the functionality for updating user profiles and registration properties. A user must be logged in in order to update his registration properties.

 

Non-blocking Method:

public void Backendless.UserService.Update( BackendlessUser user, 
                                            AsyncCallback<BackendlessUser> callback );

Blocking Method:

public BackendlessUser Backendless.UserService.Update( BackendlessUser user );

where

user - an instance of the BackendlessUser class which contains property values to update the user account with.
callback - an object which receives either a return value or an error from the server. The return value from the server is an updated instance of the BackendlessUser class.

Errors:

The following errors may occur during the Update User Properties API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Version is disabled or provided wrong application info (application id or secret key)

3018

The property marked as "identity" is being updated and another user already has the specified value which must be unique.

3024

General "update registration" error. Error message should contain additional details.

3028

User is not logged in.

3029

Cannot modify properties of another user. Returned when one user is logged and the call attempts to modify properties of another user.

3030

Unable to locate user account - invalid user id.

3031

A new "dynamic" property is being added, but dynamic property definition is disabled.

3045

Required properties in the provided object do not contain values.

Example:

// do not forget to call Backendless.initApp in the app initialization code

AsyncCallback<BackendlessUser> updateCallback = new AsyncCallback<BackendlessUser>(
        user =>
        {
          System.Console.WriteLine( "User account has been updated" );
          Dictionary<string, object> props = user.Properties;
          foreach( KeyValuePair<string, object> pair in props )
            System.Console.WriteLine( String.Format( "Property: {0} - {1}", pair.Key, pair.Value ) );
        },
        fault =>
        {
          System.Console.WriteLine( fault.ToString() );
        } );

BackendlessUser user = // user object retrieval is out of scope of this example
user.SetProperty( "phoneNumber", "5551212" );
Backendless.UserService.Update( user, updateCallback );

Get Current User

.NET applications can retrieve an instance of BackendlessUser representing the currently logged in user using the following API call:

BackendlessUser currentUser = Backendless.UserService.CurrentUser;

If a user is not logged in, the method returns null. The method also returns null, if the client application has been restarted since the last  successful login. If the last login used the "persistent login" option (the stayLoggedIn argument set to true), Backendless client saved the objectId value of the logged in user. In this case, the user object can be retrieved with the following API:

 

Blocking API:

string objectId = Backendless.UserService.LoggedInUserObjectId();
BackendlessUser user = Backendless.Data.Of<BackendlessUser>().FindById( objectId );

 

Non-Blocking API:

string objectId = Backendless.UserService.LoggedInUserObjectId();

AsyncCallback<BackendlessUser> callback = new AsyncCallback<BackendlessUser>(
  result =>
  {
    // the 'result' argument is the user object
  },

  fault =>
  {
    // server reported an error
  } );

Backendless.Data.Of<BackendlessUser>().FindById( objectId, callback );

Logout

The Logout operation terminates user session and disassociates the AuthenticatedUser role from the subsequent requests made by the client application.

 

Non-blocking Method:

public void Backendless.UserService.Logout( AsyncCallback<Object> callback )

Blocking Method:

public void Backendless.UserService.Logout();

Errors:

The following errors may occur during the Logout API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Version is disabled or provided wrong application info (application id or secret key)

3007

Invalid application-id or version.

3023

General error while executing logout. Error details should be available in the message property.

Example:

Example:

// do not forget to call Backendless.InitApp in the app initialization code 

AsyncCallback<Object> logoutCallback = new AsyncCallback<Object>(
  user =>
  {
    System.Console.WriteLine( "User has been logged out" );
  },
  fault =>
  {
    System.Console.WriteLine( fault.ToString() );
  } );

Backendless.UserService.Logout( logoutCallback );

Password Reset

Backendless supports two kinds of password reset:

1. Reset to system (backend) generated password.
2. Reset to password assigned by the user.

With the first option, an email is sent to the user with a regenerated password assigned to the user account. With the second option, an email is sent with a link to a webpage which contains a form where the user can enter the new password.

You can choose the type of reset for your application using Backendless Console. To change the password reset approach navigate to Users > Email Templates and select either User requests password recovery or User requests password recovery by link. Only one of these options can be active at a time. To make an option active, make sure the Do not send email for the event checkbox is not selected. If you make a change, make sure to click the SAVE button at the bottom of the screen.

 

For example, the following screenshot shows a configuration where the system will send out a link so the user can assign his own password. Notice the red color for the User requests password recovery option - this indicated that the option is disabled for the app:

active-mail-template.zoom80

 

In both cases (system-assigned or user-assigned passwords), the password reset sequence must initiated through the API. When the system assigns a password, it is still possible to update the user password using the Data Service API.

 

For the "password change via a link" option Backendless provides a default HTML form the link in the email points to. The HTML page is located in the File storage of the application where it can be edited and customized for branding purposes. The files are located in the /web/templates/change_password directory:

password-change-template.zoom80

 

Non-blocking Method:

   
Backendless.UserService.RestorePassword( String identity, AsyncCallback<Object> callback )

Blocking Method:

   
Backendless.UserService.RestorePassword( String identity );

where

identity - a value for the property marked as identity which uniquely identifies the user within the application.
callback - an object which is notified when the server completes the operation or returns an error.

Errors:

The following errors may occur during the Password Recovery API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Version is disabled or provided wrong application info (application id or secret key)

3020

Unable to find user with the specified login (invalid user identity).

3025

General password recovery error. Additional details should be available in the "message" property of the response.

3038

One of the requirement arguments (application id, version or user identity) is missing.

Example:

   
// do not forget to call Backendless.InitApp in the app initialization code

AsyncCallback<Object> pwRecoveryCallback = new AsyncCallback<Object>(
  user =>
  {
    System.Console.WriteLine( "Password recovery email has been sent" );
  },
  fault =>
  {
    System.Console.WriteLine( fault.ToString() );
  } );

string userEmail = "wizard@oz.org";
Backendless.UserService.RestorePassword( userEmail, pwRecoveryCallback );

User to Geo Relations

Backendless Geo Service manages application's geo location data and provides APIs to work with geopoints. Backendless supports integration between user objects managed by User Service and geopoints for the scenarios when a logical connection between the two entity types must exist in an application.

 

Relations between a user object and a geopoint are built the same way as between a data object and a geo point. The User-to-Geo integration is implemented through object relations. The Users table schema may declare a table column with a special data type - "GeoPoint Relationship". As a result, the user objects in the table may contain a reference to one or more geopoints. Backendless provides an API to establish a relationship between a user object and geopoints. Additionally, when a user object is retrieved by using the data retrieval API, any related geopoints can be retrieved by using the same mechanisms used for loading data relations. The user-to-geo relation is bidirectional, it means a geopoint may reference a user object in its metadata. You can learn more about it in the Relations with Data Objects section of the Geolocation service documentation.

 

A relation between a user object and a geopoint (or a collection of) can be established by using either the "code first" or the "schema first" approaches. With the former, the relationship is established through an API call. If the relationship is not declared in the schema, Backendless creates it based on the information in the API. With the "schema first" approach, application developer can declare a user-to-geo property first. In either one of these approaches, once a relationship is declared, user objects and geopoints may be linked together by using the Backendless console as well. For information on how to declare Users to Geo relation columns and how to link User objects with GeoPoints using Backendless console, see the Relations with Geo Objects of the Data Service documentation.

 

To create a relation both the user object and geopoint(s) must already exist in the Backendless database. It is not possible to create a relation with an object which has not been previously saved.

Creating User-to-Geo Relations with the API

Creating a relationship between a user object and a geopoint (or a collection of) uses the same API as setting/adding related objects to a data object. In the case of the user-to-geo relations, the related entity is a geopoint or a collection of geopoints.

The example below demonstrates how to assign coordinates to a user's location, that is link a user object with one or several geopoints. A user object is retrieved for the example purposes (to have an object the example will be working with).

One-to-one Relation

Blocking call

GeoPoint geoPoint = new GeoPoint( 48.85, 2.35 );
GeoPoint savedGeoPoint = Backendless.Geo.SavePoint( geoPoint );
BackendlessUser user // retrieval of user is out of scope for this example
Backendless.Data.Of<BackendlessUser>().SetRelation( user, 
                                                       "location:GeoPoint:1", 
                                                       new object[] { savedGeoPoint } );

Non-blocking call:

BackendlessUser user = // user object retrieval is out of scope for this example

AsyncCallback<object> setRelationCallback = new AsyncCallback<object>(
  result =>
  {
    System.Console.WriteLine( "User to geo relation has been set" ) ;
  },
  fault =>
  {
    System.Console.WriteLine( fault.ToString() );
  } );

AsyncCallback<GeoPoint> saveGeoPointCallback = new AsyncCallback<GeoPoint>(
  savedGeoPoint =>
  {
    Backendless.Data.Of<BackendlessUser>().SetRelation( user, 
                                                           "location:GeoPoint:1", 
                                                           new object[] { savedGeoPoint }, 
                                                           setRelationCallback );
  },
  fault =>
  {
    System.Console.WriteLine( fault.ToString() );
  } );

GeoPoint geoPoint = new GeoPoint( 48.85, 2.35 );
Backendless.Geo.SavePoint( geoPoint, saveGeoPointCallback );

Backendless Console can be used to verify the new relation. The geopoint will show up as a property in the location column of the user object.

One-to-Many Relation

For one-to-many relation, use the following column definition in the SetRelation API:

locations:GeoPoint:n

The array of child objects in that API call may contain one or more objects.

 

Security and User Roles

Application's user accounts can be mapped to roles using the API documented below. A Backendless backend contains some system roles and may include developer-defined roles. There several system roles available in all Backendless backends. These roles are automatically assigned to every user under various conditions. Each role can configured with global, container and asset permissions.

 

Core security roles are assigned based on user authentication status and whether the user logged in via "classic" backendless login or through a social network. These roles are:

Security Role

Assigned to user when an API request is made by:

NotAuthenticatedUser

...a user who is not logged in.

AuthenticatedUser

...a user who is logged in.

SocialUser

...a user who is logged in via a social network (Facebook, Twitter, Google)

FacebookUser

...a user who is logged in via Facebook

GooglePlusUser

...a user who is logged in via Google

TwitterUser

...a user who is logged in via Twitter

 

Additionally the following roles are automatically assigned by Backendless to an API request when a corresponding secret API key is used. By configuring access permissions to these roles an application may allow to custom server-side code (ServerCodeUser role), but completely reject access to the client apps (all other roles).

Security Role

Role is assigned when API uses secret key:

ASUser

ActionScript secret key

AndroidUser

Android secret key

DotNetUser

.NET secret key

IOSUser

iOS secret key

JSUser

JavaScript secret key

RestUser

REST secret key

ServerCodeUser

CodeRunner secret key

 

Backendless assigns multiple security roles for the same API request. For example, an API request made from a JavaScript application where user is not logged will have the following roles assigned: NotAuthenticatedUser and JSUser.

 

When an API call originates from business logic (Java, JS or Codeless), Backendless assigns the ServerCodeUser role.

Business logic is the only exception to the rule for assigning NotAuthenticatedUser and AuthenticatedUser roles. When business logic makes an API call and there is no authenticated user in the context of the call, Backendless assigns only the ServerCodeUser role. Otherwise, if there is an authenticated user, then both ServerCodeUser and AuthenticatedUser roles are assigned to the request.

 

Adding Roles

A developer-defined role can be added using Backendless Console:

1. Login to Console and select an application.
2. Click the Users icon.
3. Click the Security and Permissions section.
4. Click the Add Role button and enter a name for the role.
5. Click OK to save the role.

Role Permissions

A role may have multiple permissions assigned to it. A permission may either allow (GRANT) or reject (DENY) an API operation. Every operation which can be restricted with a permission has a subject. For example, the subject of a Data Service operation is a data record. For the File Service operations, the subject is a file.

All permissions are organized into a hierarchy: The global permissions apply to everything in the system. They either allow (GRANT) or reject (DENY) an API operation without distinguishing the subject. For example, a global permission may reject the Data Service's find operations for the users in the NotAuthenticatedRole. As a result, any not-authenticated user will not be able to retrieve an object from any data table in the application.

The asset container permissions inherit from the global ones and apply to a specific asset container, such as a data table. These permissions are scoped to a specific container type. For example, you may reject the get operation on file directory /privatefiles/ for the users in role X. As a result, any user who belongs to the role X will not be able to retrieve files from that directory.

Finally, the asset permissions inherit from the ones for the corresponding asset container (if they are available) or from the global set. For example, an asset permission may reject access to a specific data object for one role and grant access to the same object for another role.

The diagram below illustrates the permissions inheritance ladder:

permission-inheritance

Once a role is assigned to a user account, any permissions assigned to the role (either granting or denying an API operation) automatically apply to the operations executed by the application on behalf of the user.

Retrieving Available User Roles

This operation returns a list of the roles associated with the user account currently logged in to the Backendless application. If the request is sent without logging in, the system will return only one role - NotAuthenticatedUser.

Non-blocking Method:

Backendless.UserService.GetUserRoles( AsyncCallback<IList<string>> callback )

Blocking Method:

List<String> userRoles = Backendless.UserService.GetUserRoles();

where

callback - an object which is notified when the server returns a list of user roles or an error.

Example:

static void GetUserRoles() 
{ 
  IList<String> userRoles = Backendless.UserService.GetUserRoles(); 

  foreach( String roleName in userRoles ) 
    System.Console.WriteLine( roleName ); 
} 

When retrieving available roles without logging in, the server returns only one role - NotAuthenticatedUser.

Assigning a Role to a User:

This operation is available only from Custom Business Logic. Applications must use the "Code Runner API key" in order for the API call to be  accepted on the server. The reason for this restriction is that a malicious use of this API can easily compromise application's security. As a result, this API must be used from a controlled environment.

 

Unassigning a Role from a User:

This operation is available only from Custom Business Logic. Applications must use the "Code Runner API key" in order for the API call to be  accepted on the server. The reason for this restriction is that a malicious use of this API can easily compromise application's security. As a result, this API must be used from a controlled environment.

Errors:

The following errors may occur during the API calls described above. See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Version is disabled or provided wrong application info (application id or secret key)

2005

Could not find role.

3038

One of the required parameters (user identity or roleName) is null.

3057

Could not find user by id or identity.

3058

Could not assign role to user.

3059

Could not unassign role to user.

Global Permissions

The global service permissions apply to all operations of a Backendless service. For example, global Data Service permissions assigned to a role apply to all API operations without distinguishing the subject of an operation. (A subject is a resource targeted by an API operation - for example a data object in a "save" call).

Global permissions can be viewed and managed in Backendless Console.  After selecting an application, click Users, then Security Roles:

user-rolesv4.zoom70

 

Every Backendless service is represented by a column. Each cell in a column is a group of API operations. For example "Find" under "Data" represents all Find operations of the Data Service. The check mark in a cell indicates that that the operation is globally permitted. Each global permission has two states:

Grant - represented by a green check mark - grants the rights to execute an operation for the users in the selected role.
Deny - represented by a red X - denies the rights to execute an operation.

Asset Container Permissions

Asset Container permissions apply to all subjects within a specific container. Supported containers may be:

Data tables                - a permission assigned at that level applies to all operations on the table. For example, storing, deleting or updating objects in the table and running queries.
Messaging channels        - a permission assigned to a channel applies to all operations on the channel - publishing a message, subscribing to receive messages, etc.
File directories        - a permission assigned to a file directory applies to all file-related operations in the directory - file upload, fetching a file, etc.
Geo categories        - a permission assigned to a geo category applies to all geo point-related operation in the category - adding, deleting or updating a geo point
Media tubes        - a permission assigned to a media tube applies to all operation on media streams in the tube.

To view, assign or modify an asset container permission, use a corresponding screen in the Backendless Console. For example, to restrict access to a data table, switch to the Data view, select a table and click the Permissions menu. The user interface has two views - one is for managing user permissions and the other for roles. To modify permissions for a user account:

1. Click the User Permissions menu.
2. Enter the user name in the search field to locate the user account..
3. Click an icon representing the permission state to modify the permission.

user-permissionsv4.zoom70

Similarly permissions can be assigned or modified for specific roles - use the Role Permissions menu.

Asset Permissions

Asset permissions are the most granular in the Backendless security system as they apply to a specific object. Currently this type of permissions is available only for data objects stored in Backendless Data Service and files stored in Backendless File Service. Permissions assigned to an asset (data objects or files) supersede all other permissions - including global and asset container ones. Similar to the asset container permissions, asset permissions may apply to specific user accounts or roles. This approach becomes particularly useful when different objects/files in the same container (table or directory) should have different access levels. For example, suppose a data table called Orders contains purchase orders entered by different sales people. Every sales person should be able to see only their purchase orders, but the sales manager should see the purchase orders of his subordinates. This can be accomplished by using an asset permission scheme.

Data Service Asset Permissions

Permissions for individual data objects can be managed either with Backendless Console or using the API. To manage object's permissions with console:

1. Login to console and select an application.
2. Click Data, then select the table containing the object for which permissions should be managed.
3. There is a special "lock" icon in the ACL column for every object in the table.
acl-iconv4.zoom80
4. Click the lock icon to manage object's permissions.
5. The permissions user interface includes two menu items: Users Permissions and Roles Permissions. The former is responsible for assigning permissions for the selected object to specific users. The latter does the same but for application roles.

The screenshot below explains various parts of the user interface:

roles-permissions-v4.zoom80

The intersection of each column and a role/user either grants or denies access to the operations represented by the column. The intersections may contain the following visualizations:

ico-checkmark-disable (gray check mark)        - INHERIT. This state represents the permissions for the user/role and the operation are inherited from either asset container or global permissions.
ico-checkmark2 (green check mark) - GRANT. This state represents that the permission is granted for the user/role to perform the corresponding operation.
ico-error (red cross)                - DENY. This state represents that the permission to execute the corresponding operation is denied to the user/role.

 

Data Service API

Overview

The Backendless Data Service is a highly scalable object storage system. The system has the qualities of both a traditional structured/relational (SQL) storage and an unstructured NoSQL database. Objects are stored in tables, which have columns with assigned data types. A table can be created dynamically, when an object is being persisted and there is no table corresponding to the object's type. Similarly, when an object is saved or updated  and contains properties which do not have corresponding columns, Backendless dynamically creates the columns in the table.

Database Object Format

There are two formats for representing data objects: using custom classes or using dictionaries. With the custom class approach all properties of a class are mapped to data table columns. Instances of class are saved in a data table with the same name as the name of the class. When using dictionaries, objects are structured as key/value pairs where keys identify column names and values their respective values. See the Data Object section for additional details.

Data Relations

Data Service supports table relations which means a table may declare a column referencing another table. At the object level it is implemented through an object property which contains a reference to another object (one to one relation) or a collection of objects (one to many relations). When an object is retrieved from the server, its related objects can be retrieved in the same request (single-step retrieval) or later on through a separate request (two-step retrieval).

Query Language

Object search in Backendless can be done using the SQL syntax. A search request is sent to a specific table and a search query identifies a condition the found object must match. As a result, the query must be the format of what goes into the WHERE clause of a traditional SQL query. For more details see the Search with the Where clause section of the guide.

Real-Time Database

As objects are created, updated or deleted in the database, Backendless can deliver events in real-time to the registered listeners notifying about changes in the database. The real-time system allows synchronization between the client-side of your application and the database without any additional API requests. To learn more see the Real-Time Database section of the guide.

Column Name Mapping

When your application uses custom classes to represent data objects stored in the database, Backendless maps the column names in the database to the fields/properties declared in a custom class. For example, consider the following class:

namespace com.sample
{
  public class Person
  {
    public string name { get; set; }
    public int age { get; set; }
  }
}
When an instance of the class is saved in the Backendless database, it creates a table with the following schema:
person-table

 

As you can see the names of the columns match the names of the properties in the class. However, sometimes that mapping cannot be observed and a property in the class must have a different name or capitalization than the name of the column. In order to override the default mapping, Backendless supports an attribute which allows to map a property to a column. Consider the following example:

using System;
using Weborb.Service;

namespace com.sample
{
  public class Person
  {
    [SetClientClassMemberName( "name" )]
    public string Name { get; set; }
    
    [SetClientClassMemberName( "age" )]
    public int Age { get; set; }
  }
}

The example demonstrates the mapping of the "Name" and "Age" properties in the client application to the "name" and "age" columns in the database . The mapping is bidirectional, it means it works for both saving objects in and retrieving from the database.

Class to Table Mapping

When you use custom classes in your application for data persistence, Backendless maps the database table names to the names of the classes. For example, the following code is automatically mapped to the Person table:

namespace com.sample
{
  public class Person
  {
    public string name { get; set; }
    public int age { get; set; }
  }
}

 

The Person table in the Backendless database:

person-table

 

When the name of the class is different than the name of the corresponding table in the database, it is necessary to customize the mapping. This can be achieved with the following API:

Backendless.Data.MapTableToType( string YOUR-TABLE, Type type )

where:

YOUR-TABLE - name of the table to map to a class.
type - reference to a class to map to the table.

 

The API is not necessary if the class name is the same as the table name. The only exception to this rule is when your application retrieves related objects using the "Custom Class" approach. In this case, if application should establish mappings between client-side classes and the related (children) tables. For example, consider the following schema:

 

Order (parent) table:

parent-order-table

 

OrderItem (child) table:

child-orderitem-table

 

If any related OrderItem objects are retrieved along the parent Order object (via auto-load or single-step relation retrieval), then the application must establish a mapping for the OrderItem table before the data is retrieved.

Backendless.Data.MapTableToType( "OrderItem", typeof( OrderItem ) )

Saving Data Objects

Saving Data Objects Overview

The API to save an object in the Backendless database can be used for two separate scenarios: (1) creating new objects in the database and (2) updating existing objects. The save operation checks if the object has objectId assigned by the server. In that case, the object is updated, otherwise it is created in the Backendless database. In case when the database does not have a table for the type of the object in the API request and when Dynamic Schema Definition is enabled, Backendless creates one and maps table's columns to the object's properties.

The objectId property is automatically assigned to all objects in the database when they are initially saved. See the Data Object section for details on objectId.

Saving Single Object

Blocking Method:

Dictionary<string, object> result; 

result = Backendless.Data.Of( "TABLE-NAME" ).Save( Dictionary<string, object> );
public E Backendless.Data.Of<E>().Save( E entity );

Non-blocking Method:

void Backendless.Data.Of( "TABLE-NAME" ).Save( Dictionary<string, object> entity, 
                                               AsyncCallback<Dictionary<string, object>> responder )
public void Backendless.Data.Of<E>().Save( E entity, AsyncCallback<E> responder )

where:

TABLE-NAME - Name of the table where the object represented by System.Collections.Generic.Dictionary will be saved.
E - A .NET class of the data object to save.
entity - .NET object to persist, must be of type E or System.Collections.Generic.Dictionary (depending on the method used).
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the non-blocking method only.

Return Value:

The blocking method returns the saved object. The non-blocking call receives the return value through a callback executed on the AsyncCallback object.

Example:

 

Dictionary<string, object> contact = new Dictionary<string, object>();
contact[ "name" ] = "Jack Daniels";
contact[ "age" ] = 147;
contact[ "phone" ] = "777-777-777";
contact[ "title" ] = "Favorites";
  
// save object synchronously
Dictionary<string, object> savedContact;
savedContact = Backendless.Data.Of( "Contact" ).Save( contact );

// save object asynchronously
AsyncCallback<Dictionary<string, object>> callback;
callback = new AsyncCallback<Dictionary<string, object>>(
  result =>
  {
    // object has been saved
  },
 
  fault =>
  {
    // server reported an error
  } );
 
Backendless.Data.Of( "Contact" ).Save( contact, callback );
Consider the following class.
using System;

namespace com.mbaas
{
  public class Contact
  {
    public String objectId { get; set; }
    public String Name { get; set; }
    public int Age { get; set; }
    public String Phone { get; set; }
    public String Title { get; set; }
  }
}
The following code saves a new instance of the Contact class:
Contact contact = new Contact();
contact.Name = "Jack Daniels";
contact.Age = 147;
contact.Phone = "777-777-777";
contact.Title = "Favorites";

// save object synchronously
Contact savedContact = Backendless.Data.Of<Contact>().Save( contact );

// save object asynchronously
AsyncCallback<Contact> callback = new AsyncCallback<Contact>(
    result =>
    {
      // object has been saved
    },

    fault =>
    {
      // server reported an error
    } );
Backendless.Data.Of<Contact>().Save( contact, callback );

 

Saving Multiple Objects

This API stores multiple objects in a data table with a single request. Consider the following example, it demonstrates an API call which saves two objects in the Person data table:

IList<Dictionary<string, object>> persons = new List<Dictionary<string, object>>();

Dictionary<string, object> person1 = new Dictionary<string, object>();
person1[ "age" ] = 24;
person1[ "name" ] = "Joe";
persons.Add( person1 );

Dictionary<string, object> person2 = new Dictionary<string, object>();
person2[ "age" ] = 34;
person2[ "name" ] = "Betty";
persons.Add( person2 );

AsyncCallback<object> bulkCreateCallback = new AsyncCallback<object>(
  result =>
  {
    System.Console.WriteLine( "Objects have been saved in the database" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Person" ).Create( persons, bulkCreateCallback );

The result of running the sample above is two objects saved in the database:

bulk-create-result.zoom80

Blocking Method:

IList<string> Backendless.Data.Of( "TABLE-NAME" ).Create( 
                       IList<Dictionary<string, object>> objects );
public IList<string> Backendless.Data.Of<E>().Create( IList<E> objects );

Non-blocking Method:

void Backendless.Data.Of( "TABLE-NAME" ).Create( 
                             IList<Dictionary<string, object>> objects, 
                             AsyncCallback<IList<string>> responder )
public void Backendless.Data.Of<E>().Create( 
                          IList<E> objects, 
                          AsyncCallback<IList<string>> responder )

where:

TABLE-NAME - Name of the table where the objects will be saved.
E - A .NET class of the data objects to save.
objects - A collection of objects to store in the database.
responder - a responder object which will receive a callback when the method successfully saves the objects or if an error occurs. Applies  to the non-blocking method only.

Return Value:

The API returns a collection of object IDs for the objects created in the database. The order of  the objectId values in the response matches the order of the objects in the request..

 

The Backendless server implementation enforces the following rules:

All objects in the request must be of the same type.
Objects in the request may have different set of properties. If a column does not exist for a property, Backendless will dynamically create it, if the Dynamic Schema Definition configuration is enabled.
Objects in the request must not have objectId. If there is a value in the objectId property in a provided object, the object is ignored by the server.
Maximum number of objects in a single request is 100 for Backendless Cloud. It is configurable for Backendless Pro and Managed Backendless.
A request will be rejected by the server, if there is no Create permission granted to the user identity/roles associated with the request.

Dynamic Schema Definition

Before objects are saved in the Backendless database, the server performs several schema checks to make sure the data table is present and its schema can accommodate all properties in the objects in the API request. The API for creating new objects contains the name of a data table. If the table does not exist, Backendless automatically creates it. Each property of the object(s) in the request is validated against the table schema. If a property does not have a matching column, Backendless also automatically creates it. This behavior can be disabled using Backendless console:

1. Click the Data icon in Backendless console.
2. Click the CONFIGURATION tab.
3. Click the Enable dynamic schema definition toggle:
dynamic-schema-def

It is recommended to disable this option for performance reasons. Typically apps released into production should have Dynamic Schema Definition disabled.

Updating Data Objects

Updating Data Objects Overview

Updating a data object which has been previously stored in the Backendless data store can be done using the same API as for saving the object initially. Once an object has objectId assigned, the call to save the object will update it on the server. You can update an object individually (single object update) or several objects at a time (bulk update).

Updating Single Object

Blocking Method:

Dictionary<string, object> result; 

result = Backendless.Data.Of( "TABLE-NAME" ).Save( Dictionary<string, object> );
public E Backendless.Data.Of<E>().Save( E entity );

Non-blocking Method:

void Backendless.Data.Of( "TABLE-NAME" ).Save( Dictionary<string, object> entity, 
                                               AsyncCallback<Dictionary<string, object>> responder )
public void Backendless.Data.Of<E>().Save( E entity, AsyncCallback<E> responder )

where:

TABLE-NAME - Name of the table where the object represented by System.Collections.Generic.Dictionary will be updated.
E - A .NET class of the data object to update.
entity - .NET object to update, must be of type E or System.Collections.Generic.Dictionary (depending on the method used).
responder - a responder object which will receive a callback when the method successfully updates the object or if an error occurs. Applies  to the non-blocking method only.

Return Value:

The blocking method returns the updated object. The non-blocking call receives the return value through a callback executed on the AsyncCallback object.

Example:

Blocking API:

static void UpdateContact()
{
  // create new contact object first. Then we will update it.
  Dictionary<string, object> contact = new Dictionary<string, object>();
  contact.Add( "Name", "Jack Daniels" );
  contact.Add( "Age", 147 );
  contact.Add( "Phone", "777-777-777" );
  contact.Add( "Title", "Favorites" );

  Dictionary<string, object> savedContact = Backendless.Persistence.Of( "Contact" ).Save( contact );
      
  // now update the saved object
  savedContact[ "Title" ] = "Most favorite";
  Backendless.Persistence.Of( "Contact" ).Save( savedContact );
}

Non-blocking API

static void UpdateContactAsync()
{
  // create new contact object first. Then we will update it.
  Dictionary<string, object> contact = new Dictionary<string, object>();
  contact.Add( "Name", "Jack Daniels" );
  contact.Add( "Age", 147 );
  contact.Add( "Phone", "777-777-777" );
  contact.Add( "Title", "Favorites" );

  AsyncCallback<Dictionary<string, object>> updateObjectCallback;
  updateObjectCallback = new AsyncCallback<Dictionary<string, object>>(
    savedContact =>
    {
      System.Console.WriteLine( "object has been updated" );
    },
    error =>
    {
    }
  );

  AsyncCallback<Dictionary<string, object>> saveObjectCallback;
  saveObjectCallback = new AsyncCallback<Dictionary<string, object>>(
    savedContact =>
    {
      System.Console.WriteLine( "object has been created, now make an API call to update it" );
      // now update the saved object
      savedContact[ "Title" ] = "Most favorite";
      Backendless.Persistence.Of( "Contact" ).Save( savedContact, updateObjectCallback );
    },
    error =>
    {
    }
  );

  Backendless.Persistence.Of( "Contact" ).Save( contact, saveObjectCallback );
}
Consider the following class:
  public class Contact
  {
    //use the Weborb.Service namespace to import the annotation
    [SetClientClassMemberName( "objectId" )]   
    public String ObjectId { get;set; }
    public String Name { get; set; }
    public int Age { get; set; }
    public String Phone { get; set; }
    public String Title { get; set; }
  }
The following code saves a new instance of the Contact class and subsequently updates it:

Blocking API:

static void UpdateContactUsingClass()
{
  // create new contact object first. Then we will update it.
  Contact contact = new Contact();
  contact.Name = "Jack Daniels";
  contact.Age = 147;
  contact.Phone = "777-777-777";
  contact.Title = "Favorites";

  Contact savedContact = Backendless.Persistence.Of<Contact>().Save( contact );

  // now update the saved object
  savedContact.Title = "Most favorite";
  Backendless.Persistence.Of<Contact>().Save( savedContact );
}

Non-blocking API

static void UpdateContactAsyncUsingClass()
{
  // create new contact object first. Then we will update it.
  Contact contact = new Contact();
  contact.Name = "Jack Daniels";
  contact.Age = 147;
  contact.Phone = "777-777-777";
  contact.Title = "Favorites";

  AsyncCallback<Contact> updateObjectCallback = new AsyncCallback<Contact>(
    savedContact =>
    {
      System.Console.WriteLine( "object has been updated" );
    },
    error =>
    {
    }
  );

  AsyncCallback<Contact> saveObjectCallback = new AsyncCallback<Contact>(
    savedContact =>
    {
      System.Console.WriteLine( "object has been created, now make an API call to update it" );
      // now update the saved object
      savedContact.Title = "Most favorite";
      Backendless.Persistence.Of<Contact>().Save( savedContact, updateObjectCallback );
    },
    error =>
    {
    }
  );

  Backendless.Persistence.Of<Contact>().Save( contact, saveObjectCallback );
}

 

Updating Multiple Objects

This API updates multiple objects in a data table with a single request. Consider the following example, it demonstrates an API call which updates all objects in the Person data table where the value of the property age is greater than 20. As a result of the request, all updated objects will have the contactType property set to "personal":

Dictionary<string, object> changes = new Dictionary<string, object>();
changes[ "contactType" ] = "personal";

AsyncCallback<int> bulkUpdateCallback = new AsyncCallback<int>(
  objectsUpdated =>
  {
    System.Console.WriteLine( String.Format( "Server has updated {0} objects in the database", objectsUpdated ) );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Person" ).Update( "age > 20", changes, bulkUpdateCallback );

Blocking Method:

int Backendless.Data.Of( "TABLE-NAME" ).Update( 
                               String whereClause,
                               Dictionary<string, object> changes );
public int Backendless.Data.Of<E>().Update( 
                           String whereClause, 
                           Dictionary<string, object> changes );

Non-blocking Method:

void Backendless.Data.Of( "TABLE-NAME" ).Update( 
                      String whereClause, 
                      Dictionary<string, object> changes,
                      AsyncCallback<int> responder )
public void Backendless.Data.Of<E>().Update( 
                             String whereClause,
                             Dictionary<string, object> changes,
                             AsyncCallback<int> responder )

where:

TABLE-NAME - Name of the table where the objects are updated.
E - A .NET class of the data objects to save.
whereClause - A condition for selecting objects in the data table which will be updated.
changes - A dictionary containing the changes which will be applied to all objects in the data table which match the condition expressed via whereClause.
responder - a responder object which will receive a callback when the method successfully updates the objects or if an error occurs. Applies to the non-blocking method only.

Return Value:

The method returns the number of objects updated in the database.

 

Deleting Data Objects

Deleting Single Object

Blocking Method:

Long result = Backendless.Data.Of( "TABLE-NAME" ).Remove( String objectId );
Long result = Backendless.Data.Of( "TABLE-NAME" ).Remove( Dictionary<string, object> entity );
public long Backendless.Data.Of<E>().Remove( String objectId );
public long Backendless.Data.Of<E>().Remove( E entity );

Non-blocking Method:

void Backendless.Data.Of( "TABLE-NAME" ).Remove( String objectId, 
                                               AsyncCallback<long> responder )
void Backendless.Data.Of( "TABLE-NAME" ).Remove( Dictionary<string, object> entity, 
                                               AsyncCallback<long> responder )
public void Backendless.Data.Of<E>().Remove( String objectId, AsyncCallback<long> responder )
public void Backendless.Data.Of<E>().Remove( E entity, AsyncCallback<long> responder )

where:

TABLE-NAME - Name of the table where the object represented by System.Collections.Generic.Dictionary or objectId will be deleted.
E - A .NET class of the data object to delete.
entity - .NET object to delete, must be of type E or System.Collections.Generic.Dictionary (depending on the method used). Must contain a value for the objectId property which identifies the object to be deleted.
responder - a responder object which will receive a callback when the method successfully deletes the object or if an error occurs. Applies  to the non-blocking method only.

Return Value:

The blocking method returns the timestamp when the object is deleted. The non-blocking call receives the return value through a callback executed on the AsyncCallback object.

Example:

Blocking API:

public static void DeleteContact()
{
  // create new contact object first. Then we will delete it.
  Dictionary<String, Object> contact = new Dictionary<String, Object>();
  contact.Add( "name", "Jack Daniels" );
  contact.Add( "age", 147 );
  contact.Add( "phone", "777-777-777" );
  contact.Add( "title", "Favorites" );

  Dictionary<String, Object> savedContact = Backendless.Persistence.Of( "Contact" ).Save( contact );

  // now delete the saved object
  Backendless.Persistence.Of( "Contact" ).Remove( savedContact );
}

Non-blocking API

public static void DeleteContactAsync()
{
  // create new contact object first. Then we will delete it.
  Dictionary<String, Object> contact = new Dictionary<String, Object>();
  contact.Add( "name", "Jack Daniels" );
  contact.Add( "age", 147 );
  contact.Add( "phone", "777-777-777" );
  contact.Add( "title", "Favorites" );

  AsyncCallback<long> deleteObjectCallback = new AsyncCallback<long>(
    deletionTime =>
    {
      System.Console.WriteLine( "object has been deleted" );
    },
    error =>
    {
    }
  );

  AsyncCallback<Dictionary<String, Object>> saveObjectCallback = new AsyncCallback<Dictionary<String, Object>>(
    savedContact =>
    {
      System.Console.WriteLine( "object has been created" );
      // now delete the saved object
      Backendless.Persistence.Of( "Contact" ).Remove( savedContact, deleteObjectCallback );
    },
    error =>
    {
    }
  );

  Backendless.Persistence.Of( "Contact" ).Save( contact, saveObjectCallback );
}
Consider the following class:
package com.sample;

public class Contact
{
  private String objectId;
  private String name;
  private int age;
  private String phone;
  private String title;

  public String getObjectId() {
    return objectId;
  }

  public void setObjectId( String objectId ) {
    this.objectId = objectId;
  }

  public String getName() {
    return name;
  }

  public void setName( String name ) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge( int age ) {
    this.age = age;
  }

  public String getPhone() {
    return phone;
  }

  public void setPhone( String phone ) {
    this.phone = phone;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle( String title ) {
    this.title = title;
  }
}
The following code saves a new instance of the Contact class and subsequently updates it:

Synchronous API:

public void deleteContact()
{
    // make sure to put the initApp call somewhere early on 
    // in the app - main activity is the best place
  
    // save a new object first, so there is something to delete.
    Contact contact = new Contact();
    contact.setName( "Jack Daniels" );
    contact.setAge( 147 );
    contact.setPhone( "777-777-777" );
    contact.setTitle( "Favorites" );
    Contact savedContact = Backendless.Persistence.save( contact );

    // now delete the saved object
    Long result = Backendless.Persistence.of( Contact.class ).remove( savedContact );
 }

Asynchronous API

public void deleteContact()
{
    // put the initApp call somewhere early on in your app, perhaps main activity

    // create a new object, so there is something to delete
    Contact contact = new Contact();
    contact.setName( "Jack Daniels" );
    contact.setAge( 147 );
    contact.setPhone( "777-777-777" );
    contact.setTitle( "Favorites" );

    Backendless.Persistence.save( contact, new AsyncCallback<Contact>() 
    {
      public void handleResponse( Contact savedContact )
      {
        Backendless.Persistence.of( Contact.class ).remove( savedContact, 
                                                            new AsyncCallback<Long>() 
        {
          public void handleResponse( Long response )
          {
            // Contact has been deleted. The response is the 
            // time in milliseconds when the object was deleted
          }
          public void handleFault( BackendlessFault fault )
          {
            // an error has occurred, the error code can be 
            // retrieved with fault.getCode()
          }
        } );
      }
      @Override
      public void handleFault( BackendlessFault fault )
      {
        // an error has occurred, the error code can be retrieved with fault.getCode()
      }
    });
 }

 

Deleting Multiple Objects

This API deletes multiple objects in a data table with a single request. Consider the following example, it demonstrates an API call which deletes all objects in the Person data table where the value of the age property is greater than 20:

AsyncCallback<int> bulkDeleteCallback = new AsyncCallback<int>(
  objectsDeleted =>
  {
    System.Console.WriteLine( String.Format( "Server has deleted {0} objects in the database", objectsDeleted ) );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Person" ).Remove( "age > 20",bulkDeleteCallback );

Blocking Method:

int Backendless.Data.Of( "TABLE-NAME" ).Remove( String whereClause );
int Backendless.Data.Of<E>().Remove( String whereClause );

Non-blocking Method:

void Backendless.Data.Of( "TABLE-NAME" ).Remove( String whereClause, 
                                               AsyncCallback<int> responder )
void Backendless.Data.Of<E>().Remove( String whereClause, AsyncCallback<int> responder )

where:

TABLE-NAME - Name of the table where the objects are deleted.
E - A .NET class of the data objects to delete.
whereClause - A condition for selecting objects to be deleted in the data table.
responder - a responder object which will receive a callback when the method successfully deleted the objects or if an error occurs. Applies to the non-blocking method only.

Return Value:

The method returns the number of objects deleted in the database.

 

Retrieving Schema Definition

Backendless provides API for data table schema introspection. The API returns information about table's columns and their data types, whether a value for a column is required or if there is a default value.

Non-blocking method:

Backendless.Data.Describe( string tableName, AsyncCallback<List<ObjectProperty>> callback )

Blocking Method:

List<ObjectProperty> Backendless.Data.Describe( string tableName )

where:

tableName - name of the table to get the schema definition for.

Return value:

Method returns a collection of the ObjectProperty objects. The ObjectProperty class includes the following properties:

autoLoad:boolean - applies only to relations. If true, the property is set to auto-load related data for the data retrieval queries.
CustomRegex:String - a regular expression assigned to the column as a validator. The validator applies when a new object is saved in the table or an existing one is updated.
DefaultValue:Object - a default value assigned to any object saved/updated in the table where the column does not have a value.
PrimaryKey:bool - true if the column is or is a part of a primary key.
Name:String - contains the name of a property.
RelatedTable:String - contains the name of the related table(s).
IsRequired:boolean - defines whether a property is optional or required for the requests which save the initial object or update an existing one.
Type:DateTypeEnum - defines the column type.

Example:

Suppose the Person table has the following schema - there are two data columns, age and name.

person-schema.zoom70

The following code retrieves the schema definition for the Person table:

List<ObjectProperty> properties = Backendless.Persistence.Describe( "Person" ); 

foreach( ObjectProperty property in properties ) 
{ 
  System.Console.WriteLine( "property name - " + propDef.Name );
  System.Console.WriteLine( "\tis property required - " + propDef.IsRequired );
  System.Console.WriteLine( "\tproperty data type - " + propDef.Type );
  System.Console.WriteLine( "\tdefault value - " + propDef.DefaultValue );
  System.Console.WriteLine( "\tis property identity - " + propDef.PrimaryKey );
  System.Console.WriteLine( "=====================================" ); 
}  

The code produces the following output:

property name - objectId
  is property required - false
  property data type - STRING_ID
  default value - null
  is property primary key - true
property name - age
  is property required - false
  property data type - INT
  default value - null
  is property primary key - false
property name - updated
  is property required - false
  property data type - DATETIME
  default value - null
  is property primary key - false
property name - name
  is property required - false
  property data type - STRING
  default value - null
  is property primary key - false
property name - created
  is property required - false
  property data type - DATETIME
  default value - null
  is property primary key - false
property name - ownerId
  is property required - false
  property data type - STRING
  default value - null
  is property primary key - false

 

Get Object Count

The Object Count API provides a way to obtain the following values from the server:

Number of objects in a table
Number of objects matching query
Number of related objects

 

Non-blocking Method:

// get object count for all objects in the table
Backendless.Data.Of( "TABLE-NAME" ).GetObjectCount( AsyncCallback<int> callback );

// get object count for all objects in the table which match the query
Backendless.Data.Of( "TABLE-NAME" ).GetObjectCount( 
                                                   DataQueryBuilder queryBuilder,
                                                   AsyncCallback<int> callback );
// get object count for all objects in the table
Backendless.Data.Of( E ).GetObjectCount( AsyncCallback<int> callback );

// get object count for all objects in the table which match the query
Backendless.Data.Of( E ).GetObjectCount( DataQueryBuilder queryBuilder,
                                         AsyncCallback<int> callback );

Blocking Method:

// get object count for all objects in the table
int Backendless.Data.Of( "TABLE-NAME" ).GetObjectCount();

// get object count for all objects in the table which match the query
int Backendless.Data.Of( "TABLE-NAME" ).GetObjectCount( DataQueryBuilder queryBuilder );
// get object count for all objects in the table
public int Backendless.Data.of( E ).getObjectCount();

// get object count for all objects in the table which match the query
public int Backendless.Data.of( E ).getObjectCount( DataQueryBuilder queryBuilder );

where:

TABLE-NAME - Name of the table where to calculate the object count.
E - a .NET class which identifies the table where to calculate the object count.
queryBuilder - Instance of BackendlessAPI.Persistence.DataQueryBuilder. When present in the arguments, must contain a whereClause query. The query is used by the server to identify a collection of objects to calculate the count of.
callback - a callback object which will receive a callback when the method successfully calculates the object count. Applies to the non-blocking methods only.

Return Value:

The blocking methods returns an integer value which is the object count. Non-blocking methods receive a callback with the count value.

Example:

Total object count for a table:

The following sample request retrieves total number of objects in table Order:

AsyncCallback<int> getCountCallback = new AsyncCallback<int>(
  objectCount => 
    {
      System.Console.WriteLine( "object count is " + objectCount );
    },
  error => 
    {
      System.Console.WriteLine( "Server returned an error " + error.Message );
    }
  );
Backendless.Data.Of( "Order" ).GetObjectCount( getCountCallback );
AsyncCallback<int> getCountCallback = new AsyncCallback<int>(
  objectCount => 
    {
      System.Console.WriteLine( "object count is " + objectCount );
    },
  error => 
    {
      System.Console.WriteLine( "Server returned an error " + error.Message );
    }
  );
Backendless.Data.Of<Order>().GetObjectCount( getCountCallback );

Object count for a query:

The following sample request retrieves total number of objects in table Order which satisfy the condition of orderAmount > 100:

AsyncCallback<int> getCountCallback = new AsyncCallback<int>(
  objectCount => 
    {
      System.Console.WriteLine( "object count is " + objectCount );
    },
  error => 
    {
      System.Console.WriteLine( "Server returned an error " + error.Message );
    }
  );

DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetWhereClause( "orderAmount > 100" );
Backendless.Data.Of( "Order" ).GetObjectCount( queryBuilder, getCountCallback );
AsyncCallback<int> getCountCallback = new AsyncCallback<int>(
  objectCount => 
    {
      System.Console.WriteLine( "object count is " + objectCount );
    },
  error => 
    {
      System.Console.WriteLine( "Server returned an error " + error.Message );
    }
  );

DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetWhereClause( "orderAmount > 100" );
Backendless.Data.Of<Order>().GetObjectCount( queryBuilder, getCountCallback );

Related object count:

The following sample request retrieves total number of related "child" objects for a parent object. The parent table is Person. It contains a relation column called address pointing to the Address table. The query below retrieves a count of related child objects for a parent object with objectID of XXXX-XXXX-XXXX-XXXX. The whereClause query syntax for this scenario is:
 
Person[address].objectId = 'XXXX-XXXX-XXXX-XXXX'

AsyncCallback<int> getCountCallback = new AsyncCallback<int>(
  objectCount => 
    {
      System.Console.WriteLine( "object count is " + objectCount );
    },
  error => 
    {
      System.Console.WriteLine( "Server returned an error " + error.Message );
    }
  );
  
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetWhereClause( "Person[address].objectId = 'XXXX-XXXX-XXXX-XXXX'" );
Backendless.Data.Of( "Address" ).GetObjectCount( queryBuilder,
                                                 getCountCallback );
AsyncCallback<int> getCountCallback = new AsyncCallback<int>(
  objectCount => 
    {
      System.Console.WriteLine( "object count is " + objectCount );
    },
  error => 
    {
      System.Console.WriteLine( "Server returned an error " + error.Message );
    }
  );
  
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetWhereClause( "Person[address].objectId = 'XXXX-XXXX-XXXX-XXXX'" );
Backendless.Data.Of<Address>().GetObjectCount( queryBuilder,
                                                  getCountCallback );

 

Basic Object Retrieval

Backendless supports multiple data search and retrieval operations. These include finding an object by its objectId, finding first or last object in the collection or retrieving the entire persisted collection. Each method is available in both blocking and non-blocking formats:

Retrieving Data Objects

Blocking Methods:

Retrieve data objects with the default paging setting from a table:
IList<Dictionary<string, object>> Backendless.Data.Of( "TABLE-NAME" ).Find()
Find first data object in a table. The first data object is the first one saved in the database:
Dictionary<string, object> Backendless.Data.Of( "TABLE-NAME" ).FindFirst() 
Find last data object from a table. The last data object is the last one saved in the database:
Dictionary<string, object> Backendless.Data.Of( "TABLE-NAME" ).FindLast() 
Find a data object by its objectId:
Dictionary<string, object> Backendless.Data.Of( "TABLE-NAME" ).FindById( string objectId ) 
Retrieve data objects with the default paging setting from a table. Returned collection will contain objects of type E (the name of the class must match the name of the table):
IList<E> Backendless.Data.Of<E>().Find() 
Find first data object of class E. The first data object is the first one saved in the database:
E Backendless.Data.Of<E>().FindFirst() 
Find last data object of type E. The last data object is the last one saved in the database:
E Backendless.Data.Of<E>().FindLast() 
Find a data object by its objectId:
E Backendless.Data.Of<E>().FindById( string objectId ) 

Non-blocking Methods:

Retrieve data objects with the default paging setting from a table:
public void Backendless.Data.Of( "TABLE-NAME" ).Find( 
                AsyncCallback<IList<Dictionary<string, object>>> callback )
Find first data object in a table. The first data object is the first one saved in the database:
public void Backendless.Data.Of( "TABLE-NAME" ).FindFirst( 
                AsyncCallback<Dictionary<string, object>> callback )
Find last data object in a table. The last data object is the last one saved in the database:
public void Backendless.Data.Of( "TABLE-NAME" ).FindLast( 
                AsyncCallback<Dictionary<string, object>> callback ) 
Find a data object by its objectId:
public void Backendless.Data.Of( "TABLE-NAME" ).FindById( 
                string objectId, 
                AsyncCallback<Dictionary<string, object>> callback ) 
Retrieve data objects with the default paging setting from a table. Returned collection will contain objects of type E (the name of the class must match the name of the table):
public void Backendless.Data.Of<E>().Find( AsyncCallback<IList<E>> callback );
Find first data object in a table. The name of the class E must match the name of the table. The first data object is the first one saved in the database:
public void Backendless.Data.Of<E>().FindFirst( AsyncCallback<E> callback )
Find last data object in a table. The name of the class E must match the name of the table. The last data object is the last one saved in the database:
public void Backendless.Data.Of<E>().FindLast( AsyncCallback<E> callback )
Find a data object by its objectId. The name of the class E must match the name of the table:
public void Backendless.Data.Of<E>().FindById( string objectId, 
                                                  AsyncCallback<E> callback )

where:

TABLE-NAME - Name of the table from where the data is retrieved from.
E - a .NET class identifying the table where from where the data must be loaded from.
callback - a callback object which will receive a callback when the method successfully returns a result or an error. Applies to the non-blocking methods only.

Example:

The following code demonstrates various search queries:

Load contacts using default paging:

Blocking call:
IList<Dictionary<string, object>> result = Backendless.Data.Of( "Contact" ).Find();
Non-blocking call:
AsyncCallback<IList<Dictionary<string, object>>> callback;
callback = new AsyncCallback<IList<Dictionary<string, object>>>(
  foundContacts =>
  {
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
Backendless.Data.Of( "Contact" ).Find( callback );

Find first contact:

Blocking call:
Dictionary<string, object> firstContact = Backendless.Data.Of( "Contact" ).FindFirst();
Non-blocking call:
AsyncCallback<Dictionary<string, object>> callback;
callback = new AsyncCallback<Dictionary<string, object>>(
  foundContacts =>
  {
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Contact" ).FindFirst( callback );

Find last contact:

Blocking call:
Dictionary<string, object> lastContact = Backendless.Data.Of( "Contact" ).FindLast();
Non-blocking call:
AsyncCallback<Dictionary<string, object>> callback;
callback = new AsyncCallback<Dictionary<string, object>>(
  foundContacts =>
  {
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Contact" ).FindLast( callback );

Find contact by objectId:

Blocking call:
// Save a contact object first. Once it is saved, we will retrieve it by the assigned objectId
Dictionary<string, object> contact = new Dictionary<string, object>();
contact.Add( "name", "Jack Daniels" );
contact.Add( "age", 147 );
contact.Add( "phone", "777-777-777" );
contact.Add( "title", "Favorites" );

// save object synchronously
Dictionary<string, object> savedContact = Backendless.Data.Of( "Contact" ).Save( contact );

// now retrieve the object using it's objectId
Dictionary<string, object> foundContact = Backendless.Data.Of( "Contact" ).
                                    FindById( (string) savedContact[ "objectId" ] );
Non-blocking call:
// Save a contact object first. Once it is saved, we will retrieve it by the assigned objectId
Dictionary<string, object> contact = new Dictionary<string, object>();
contact.Add( "name", "Jack Daniels" );
contact.Add( "age", 147 );
contact.Add( "phone", "777-777-777" );
contact.Add( "title", "Favorites" );

AsyncCallback<Dictionary<string, object>> getObjectCallback = 
new AsyncCallback<Dictionary<string, object>>(
foundContact =>
{
},
error =>
{
  System.Console.WriteLine( "Server returned an error " + error.Message );
} );

AsyncCallback<Dictionary<string, object>> saveObjectCallback = 
new AsyncCallback<Dictionary<string, object>>(
savedContact =>
{
  // now retrieve the object using it's objectId
  Backendless.Data.Of( "Contact" ).FindById( (string) savedContact[ "objectId" ], getObjectCallback );

},
error =>
{
  System.Console.WriteLine( "Server returned an error " + error.Message );
} );

// save object asynchronously
Backendless.Data.Of( "Contact" ).Save( contact, saveObjectCallback );
Consider the following class:
public class Contact
{
  public string objectId { get; set; }
  public string name { get; set; }
  public int age { get; set; }
  public string phone { get; set; }
  public string title { get; set; }
}
The following code demonstrates various search queries using the class:

Load contacts using default paging:

Blocking call:
IList<Contact> result = Backendless.Data.Of<Contact>().Find();
Non-blocking call:
AsyncCallback<IList<Contact>> getContactsCallback = 
  new AsyncCallback<IList<Contact>>(
    foundContacts =>
    {
    },
    error =>
    {
      System.Console.WriteLine( "Server returned an error " + error.Message );
    } );

Backendless.Data.Of<Contact>().Find( getContactsCallback );

Find first contact:

Blocking call:
Contact firstContact = Backendless.Data.Of<Contact>().FindFirst();
Non-blocking call:
AsyncCallback<Contact> getContactCallback = 
  new AsyncCallback<Contact>(
    foundContact =>
    {
    },
    error =>
    {
      System.Console.WriteLine( "Server returned an error " + error.Message );
    });

Backendless.Data.Of<Contact>().FindFirst( getContactCallback );

Find last contact:

Blocking call:
Contact lastContact = Backendless.Data.Of<Contact>().FindLast();
Non-blocking call:
AsyncCallback<Contact> getContactCallback = 
  new AsyncCallback<Contact>(
    foundContact =>
    {
    },
    error =>
    {
      System.Console.WriteLine( "Server returned an error " + error.Message );
    });

Backendless.Data.Of<Contact>().FindLast( getContactCallback );

Find contact by objectId:

Blocking call:
// Save a contact object first. Once it is saved, we will retrieve it by assigned objectId
Contact contact = new Contact();
contact.Name = "Jack Daniels";
contact.Age = 147;
contact.Phone = "777-777-777";
contact.Title = "Favorites";
 
// save object synchronously
Contact savedContact = Backendless.Data.Of<Contact>().Save( contact );

// now retrieve the object using it's objectId
Contact foundContact = Backendless.Data.Of<Contact>().FindById( savedContact.ObjectId );
Non-blocking call:
// Save a contact object first. Once it is saved, we will retrieve it by assigned objectId
Contact contact = new Contact();
contact.Name = "Jack Daniels";
contact.Age = 147;
contact.Phone = "777-777-777";
contact.Title = "Favorites";
 
AsyncCallback<Contact> loadContactCallback = new AsyncCallback<Contact>(
  foundContact =>
  {
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

AsyncCallback<Contact> saveContactCallback = new AsyncCallback<Contact>(
  savedContact =>
  {
    Backendless.Data.Of<Contact>().FindById( savedContact.ObjectId );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

// save object asynchronously
Backendless.Data.Of<Contact>().Save( contact, saveContactCallback );

 

Advanced Object Retrieval

General API

Advanced search use-cases supported by Backendless include:

Search with a query (a "where clause" search) -retrieves data objects which satisfy a condition.
Paged data retrieval - retrieves a "page" of data of the given size from the specified offset.
Sorted data object retrieval - retrieves a collection of data objects sorted by specified properties.
Retrieval of related objects - fetching data objects through special "relation" properties. See retrieval of related data objects.
Calculating aggregate values for a collection of objects - retrieves sum, average, min, max or count for all or a subset of objects in a table. See Aggregate Functions for more details.

 

Backendless supports the options listed above with a special class - DataQueryBuilder. The class includes various properties to configure paging and sorting, specify a search query (the where clause), and/or to request retrieval of objects for specific related properties. The DataQueryBuilder class provides the following methods:

Create - creates a new instance of DataQueryBuilder.
SetWhereClause - sets a search query. A query must be in the SQL-92 syntax (the "where" clause part).
SetSortBy - sets a collection of column names to sort the data objects in the response by.
SetRelated - sets an collection of related columns names. Objects from the related columns are included into the response. For information about relation retrieval, see the Relations (Retrieve) chapter of the documentation.
SetRelationsDepth - sets the number of "levels" in the hierarchy of related objects to include into the response.
SetPageSize - sets the page size - which is the number of objects to return in the response. Maximum value is 100. For more information on paging, see the Data retrieval with Paging section.
SetOffset - sets the offset - an index in the server-side storage from where the data objects should be retrieved.
SetGroupBy - Used with Aggregate Functions. Sets the name of a column to group the results by.
AddGroupBy - Used with Aggregate Functions. Add the name of the columns to the existing groupBy collection to group the results by.
SetHavingClause - Used with Aggregate Functions. Sets a condition on a aggregate function to filter groups.

 

Method Signature:

Blocking Method:

// Return value from the server is a collection of java.util.Map objects; 
IList<Dictionary<string, object>> result;

// The data table must be identified by name. The API
// returns a collection of Dictionary objects.
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();

// set query builder properties to indicate what should be retrieved from the server
queryBuilder..... = 

result = Backendless.Data.Of( "TABLE-NAME" ).Find( queryBuilder );
IList<E> result = Backendless.Data.Of<E>().Find( DataQueryBuilder queryBuilder );

Non-blocking Method:

Backendless.Data.Of( "TABLE-NAME" ).Find( 
                      DataQueryBuilder query, 
                      AsyncCallback<IList<Dictionary<string, object>>> );
Backendless.Data.Of<E>().Find( DataQueryBuilder queryBuilder, 
                                  AsyncCallback<IList<E>> );

where:

TABLE-NAME - Name of the table to retrieve data from.
E - a .NET class identifying the table to retrieve data from. The name of the class must match the name of the table.
queryBuilder - an instance of DataQueryBuilder - contains the search query and other search options.
responder - a responder object which will receive a callback when the search operation returns the result. Applies to the non-blocking method only.

Return Value:

The blocking method returns a collection of objects found as a result of the query execution. The non-blocking call receives the return value through a callback executed on the AsyncCallback object.

Examples

For examples on using the advanced object retrieval API, see the following sub-sections:

Search with the Where Clause

A where clause is a condition which references columns of a data table where the query is sent. For any given query Backendless returns the data records which match the condition in the where clause. A where clause is a query string which must conform to a subset of the SQL-92 standard. The subset of the syntax supported by Backendless is the part that goes into the WHERE clause of an SQL query. This is the reason why we refer to it as the where clause. For example, consider the following data objects, specifically notice the columns name and age:

sample-table.zoom70

 

Suppose you need to retrieve an object where the value of the name property is "Joe". This can be accomplished with the following where clause:

name = 'Joe'

 

Or if you need all objects where the age column contains a value less than 30:

age < 30

 

Just like in SQL, columns can be combined in a single where clause. The following condition would retrieve all objects containing letter J in the name and the age column's value is greater than 20:

name LIKE 'J%' and age > 20

 

Condition

Sample where clause

Column type

Value in a column equals a string literal.

name = 'foo'

STRING, TEXT or EXTENDED STRING

Value in a column does not equal a string literal.

name != 'foo'

STRING, TEXT or EXTENDED STRING

Value in a column is not assigned

name is null

STRING, TEXT or EXTENDED STRING

Value in a column is assigned

name is not null

STRING, TEXT or EXTENDED STRING

Value in a column contains a substring literal

name LIKE '%foo%'

STRING, TEXT or EXTENDED STRING

Value in a column ends with a substring literal

name LIKE '%foo'

STRING, TEXT or EXTENDED STRING

Value in a column starts with a substring literal

name LIKE 'foo%'

STRING, TEXT or EXTENDED STRING

Value in a column is one of enumerated string literals.

name IN ('value1', 'value2', 'value3)
 
or
 
name = 'value1' OR name = 'value2' or name = 'value3'

STRING, TEXT or EXTENDED STRING

Value in a column equals a numeric literal

age = 21

INT or DOUBLE

Value in a column is greater than a numeric literal

age > 21

INT or DOUBLE

Value in a column is greater than or equal a numeric literal

age >= 21

INT or DOUBLE

Value in a column is less than a numeric literal

age < 21

INT or DOUBLE

Value in a column is less than or equal a numeric literal

age <= 21

INT or DOUBLE

 

You do not need to declare/write any custom classes when using the Dictionary approach. Data objects are represented as System.Collections.Generic.Dictionary objects. Column names become map property names and the object values are corresponding property values.
Consider the following class:
namespace com.sample 
{
  public class Contact
  {
    [SetClientClassMemberName( "objectId" )]
    public String ObjectId {get; set;};
    public String Name {get; set;};
    public int Age {get; set;};
    public String Phone {get; set;};
    public String Title {get; set;};
  }
}

 

Find all contacts where the value of the "age" property equals 47:

String whereClause = "age = 47";
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Dictionary<string, object>> result = Backendless.Data.Of( "Contact" ).Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Dictionary<string, object>>> findCallback;
findCallback = new AsyncCallback<IList<Dictionary<string, object>>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Contact" ).Find( queryBuilder, findCallback );
String whereClause = "age = 47";
DataQueryBuilder queryBuilder = new DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Contact> result = Backendless.Data.Of<Contact>().Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Contact>> findCallback;
findCallback = new AsyncCallback<IList<Contact>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of<Contact>().Find( queryBuilder, findCallback );

Find all contacts where the value of the "age" property is greater than 21:

String whereClause = "age > 21";
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Dictionary<string, object>> result = Backendless.Data.Of( "Contact" ).Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Dictionary<string, object>>> findCallback;
findCallback = new AsyncCallback<IList<Dictionary<string, object>>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Contact" ).Find( queryBuilder, findCallback );
String whereClause = "age > 21";
DataQueryBuilder queryBuilder = new DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Contact> result = Backendless.Data.Of<Contact>().Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Contact>> findCallback;
findCallback = new AsyncCallback<IList<Contact>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of<Contact>().Find( queryBuilder, findCallback );

Find all contacts by name:

String whereClause = "name = 'Jack Daniels'";
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Dictionary<string, object>> result = Backendless.Data.Of( "Contact" ).Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Dictionary<string, object>>> findCallback;
findCallback = new AsyncCallback<IList<Dictionary<string, object>>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Contact" ).Find( queryBuilder, findCallback );
String whereClause = "name = 'Jack Daniels'";
DataQueryBuilder queryBuilder = new DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Contact> result = Backendless.Data.Of<Contact>().Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Contact>> findCallback;
findCallback = new AsyncCallback<IList<Contact>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of<Contact>().Find( queryBuilder, findCallback );

Find all contacts by partial name match:

String whereClause = "name LIKE 'Jack%'";
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Dictionary<string, object>> result = Backendless.Data.Of( "Contact" ).Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Dictionary<string, object>>> findCallback;
findCallback = new AsyncCallback<IList<Dictionary<string, object>>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Contact" ).Find( queryBuilder, findCallback );
String whereClause = "name LIKE 'Jack%'";
DataQueryBuilder queryBuilder = new DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Contact> result = Backendless.Data.Of<Contact>().Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Contact>> findCallback;
findCallback = new AsyncCallback<IList<Contact>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of<Contact>().Find( queryBuilder, findCallback );

 

Search with SubQuery

Search with subquery allows your apps to run two queries in the context of a single data retrieval request. The first query identifies a set of objects which is fed into the second query. Consider the following data schema:

users-blogpost-comment

Suppose your app needs to get all blog posts where the person who posted a comment has a specific email address (for example, @backendless.com ). Using subqueries, it can be done with the following whereClause sent to the BlogPost table:
 

objectId in (Comment[author.email LIKE '%@backendless.com'].blogPost.objectId)

 

Let's review how this whereClause is processed by Backendless:

 

1. Backendless detects that the query at the top level uses the in()  operator.
2. It checks if the contents of the in() operator have the format of a subquery and if so, evaluates it accordingly.
3. The subquery contains an "internal whereClause", which is:
author.email LIKE '%@backendless.com'
4. The internal whereClause is applied to the objects in the Comment table.
5. For all found Comment objects, Backendless gets a list of values for the blogPost.objectId column.
6. The resulting set is fed into the in() operator which fetches the final set of objects from the BlogPost table.

 

General SubQuery Syntax

The general syntax for subqueries is:

 

searchColumnNameOrRelatedColumnName IN
  (TableName[internalWhereClause].columnOrRelatedColumnName.optionalcolumnName)

 

How it works:

1. If internalWhereClause  is present, it is executed in the table identified by TableName.
2. In the resulting set of records, values must be selected for the column identified by
columnOrRelatedColumnName.optionalColumnName.
3. If internalWhereClause is not present, the contents of the IN operator have the following syntax. It represents the entire set of values identified by the specified column: TableName.columnOrRelatedColumnName.optionalColumnName
4. The resulting set of values is used in the IN operator, thus the final query ends up being:
searchColumnOrRelatedColumnName IN ( value1, value2,,,valueN )

 

Additional Examples

 

Consider the following data schema:

customer-order-items

1. Get all OrderItem  objects for a customer from New York:
objectId in (Order[customer.address = 'New York'].items.objectId)
 
2. Get all Customer objects who spent more than 1.99 on an item:
objectId in (Order[items.itemPrice > 1.99].customer.objectId)

Using Dates in Search

There is a special consideration for the whereClause-based queries which reference a column of the DATETIME data type. Typically a DATETIME column is referenced in a comparison against a scalar value describing a specific date or a timestamp. The scalar value can be a number of milliseconds since the epoch (UNIX timestamp as milliseconds) or a string. Backendless supports a variety of date formats for the latter. For example, the queries below will find all the objects which were updated after March 23rd, 2015:

updated > '23-Mar-2015'

updated > '03/23/2015'

updated > 1427068800000

Comparison Operators

Backendless supports the following date comparison operators:

Column's value is after the specified date/time: use either > or the after keyword:

birthDate > '22-Dec-1980'

birthDate after 1427068800000

Column's value is before the specified date/time: use either < or the before keyword:

birthDate < '22-Dec-1980'

birthDate before 1427068800000

Column's value is either at or after the specified date/time: use either => or the at or after keyword:

birthDate >= '28-10-1988'

birthDate at or after '10/28/1988 00:00:00 GMT-0200'

Column's value is either at or before the specified date/time: use either <= or the at or before keyword:

birthDate >= '28-10-1988'

birthDate at or after '10/28/1988 00:00:00 GMT-0200'

Note: the whereClause-based queries can be tested in the Backendless Console with the SQL Search turned on.

Supported Date Formats

Date/time string values may be in any of the following formats. The pattern letters have the same definition as in Java's SimpleDateFormat:

EEE MMM dd HH:mm:ss zzz yyyy
yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
MM/dd/yyyy HH:mm:ss 'GMT'z
MM.dd.yyyy HH:mm:ss 'GMT'z
MM-dd-yyyy HH:mm:ss 'GMT'z
MM/dd/yyyy HH:mm:ss z
MM.dd.yyyy HH:mm:ss z
MM.dd.yyyy HH:mm:ss
MM-dd-yyyy HH:mm:ss
MM/dd/yyyy HH:mm:ss
MM.dd.yyyy
MM-dd-yyyy
MM/dd/yyyy HH:mm:ss 'GMT'Z
MM/dd/yyyy HH:mm
MM/dd/yyyy
dd/MMM/yyyy
dd-MMM-yyyy
EEEEE, d MMMMM yyyy
yyyy/MM/d/HH:mm:ss
yyyy-MM-dd'T'HH:mm:ss
EEEEE, MMMMM d, yyyy
MMMMM d, yyyy
yyyy M d
yyyyMMMd
yyyy-MMM-d
yyyy-M-d, E
'Date' yyyy-MM-dd
yyyy-MM-dd'T'HH:mm:ssZ
yyyy-MM-dd'T'HH:mmZ
yyyy-MM-dd
yyyy-'W'w
yyyy-DDD
d MMMMM yyyy, HH'h' mm'm' ss's'

Example

String whereClause = "updated after " + DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Dictionary<string, object>> result = Backendless.Data.Of( "Contact" ).Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Dictionary<string, object>>> findCallback;
findCallback = new AsyncCallback<IList<Dictionary<string, object>>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Contact" ).Find( queryBuilder, findCallback );
String whereClause = "updated after " + DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
DataQueryBuilder queryBuilder = new DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Contact> result = Backendless.Data.Of<Contact>().Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Contact>> findCallback;
findCallback = new AsyncCallback<IList<Contact>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of<Contact>().Find( queryBuilder, findCallback );

 

Data retrieval with Paging

Backendless operations returning a collection of objects automatically break up the complete result set into pages. Backendless paging uses two parameters to enable paged data retrieval:

page size - identifies how many objects a single page contains
offset - identifies the position of the object from which to retrieve the number of records identified by page size

 

The diagrams below illustrate the paging process. The first diagram shows the first request to retrieve a collection of records. The request includes the parameters of 5 objects in the page, starting from offset 0:

findrequest1

 

To retrieve the next page of data, the client must set the offset to the index of the first not retrieved object, which is 5:

findrequest2

 

Backendless server sets the maximum allowed value for the page size to 100 objects. The minimum value is 1.

 

.NET applications must use the DataQueryBuilder class to set the paging parameters. The example below configures DataQueryBuilder to load 25 objects starting from offset 50. The query is used to retrieve objects from the Person table:

DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetPageSize( 25 ).SetOffset( 50 );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Dictionary<string, object>> result = Backendless.Data.Of( "Contact" ).Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Dictionary<string, object>>> findCallback;
findCallback = new AsyncCallback<IList<Dictionary<string, object>>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Contact" ).Find( queryBuilder, findCallback );
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetPageSize( 25 ).SetOffset( 50 );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Contact> result = Backendless.Data.Of<Contact>().Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Contact>> findCallback;
findCallback = new AsyncCallback<IList<Contact>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of<Contact>().Find( queryBuilder, findCallback );

Application can use the same queryBuilder object to recalculate offset (and if necessary change page size) and use it to retrieve additional pages from the server using the same data retrieval method:

// calculate offset to get the next page of data, then re-run the query
queryBuilder.PrepareNextPage();

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Dictionary<string, object>> result = Backendless.Data.Of( "Contact" ).Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Dictionary<string, object>>> findCallback;
findCallback = new AsyncCallback<IList<Dictionary<string, object>>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Contact" ).Find( queryBuilder, findCallback );
// calculate offset to get the next page of data, then re-run the query
queryBuilder.PrepareNextPage();

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Contact> result = Backendless.Data.Of<Contact>().Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Contact>> findCallback;
findCallback = new AsyncCallback<IList<Contact>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of<Contact>().Find( queryBuilder, findCallback );

 

 

 

Sorting

Data retrieval and search API can request the server to return sorted data. Data can be sorted by one or more columns. The sorting direction can be either ascending (default) or descending for each individual column. .NET applications must use the DataQueryBuilder class to set the sorting option. The example below configures DataQueryBuilder to sort results by the name and age columns. Value in the age column are sorted in the descending order. The query is used to retrieve objects from the Person table:

DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.AddSortBy( "name" );
queryBuilder.AddSortBy( "age DESC" );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Dictionary<string, object>> result = Backendless.Data.Of( "Contact" ).Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Dictionary<string, object>>> findCallback;
findCallback = new AsyncCallback<IList<Dictionary<string, object>>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Contact" ).Find( queryBuilder, findCallback );
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.AddSortBy( "name" );
queryBuilder.AddSortBy( "age DESC" );

// ***********************************************************
// Blocking API:
// ***********************************************************
IList<Contact> result = Backendless.Data.Of<Contact>().Find( queryBuilder );

// ***********************************************************
// Non-blocking API:
// ***********************************************************
AsyncCallback<IList<Contact>> findCallback;
findCallback = new AsyncCallback<IList<Contact>>(
  foundObjects =>
  {
    System.Console.WriteLine( "Server returned " + foundObjects.Count + " objects" );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of<Contact>().Find( queryBuilder, findCallback );

The descending sorting order can be requested by adding DESC to the name of the column (separated by space).

Search by distance

With the ability to link data objects with geo points, you can search for data objects by distance. This type of search returns a paged set of data objects which satisfy the whereClause condition and located within the specified distance. Distance-based search uses a special function in whereClause of the search request. The syntax of the function is:

distance( 
   center point latitude, 
   center point longitude, 
   columnname which contains geo point.latitude,
   columnname which contains geo point.longitude )<operator> units-function(value)

where:

<operator> - Possible values are <, >, =, >=, <=
units-function - Defines the units of measure for the distance. Possible values are:
ft( X ) - the distance value X is expressed in feet
km( X ) - the distance value X is expressed in kilometers
mi( X ) - the distance value X is expressed in miles
yd( X ) -  the distance value X is expressed in yards

For example, the following whereClause expression searches for data objects located within 200 miles from the point at 30.26715, -97.74306. Each data object must have the "coordinates" property of type GeoPoint.

distance( 30.26715, -97.74306, coordinates.latitude, coordinates.longitude ) < mi(200)

The following example demonstrates a search-by-distance query. The example uses three data objects stored in the Friend table: Bob, Jane, and Fred who respectively live in Austin, Houston, San Antonio. The search query in the example finds all friends who live within the specified distance. Before running the search query, create the objects in the data storage with the corresponding geo points.

 

The application must be setup with demo data in order to run the example and see the distance operator in action. See the setup code in the expandable block below:

The Friend class definition:

You do not need to declare/write any custom classes when using the Dictionary approach. Database objects are represented as Dictionary<string, object> objects. Column names become dictionary key names and the object values are the corresponding values.
The Friend class definition:
class Friend
  {
    public string Name { get; set; }
    public String PhoneNumber { get; set; }
    public GeoPoint Coordinates { get; set; }
  }

 

Run the following query/code to store a data object representing Bob with a link to his home in Austin, TX:

Dictionary<string, object> bob = new Dictionary<string, object>();
bob[ "name" ] = "Bob";
bob[ "phoneNumber" ] = "512-555-1212";
bob = Backendless.Data.Of( "Friend" ).Save( bob );

GeoPoint geopoint = new GeoPoint( 29.76328, -95.36327 );
geopoint.Categories.Add( "Home" );
geopoint.Metadata.Add( "description", "Bob's home" );
geopoint = Backendless.Geo.SavePoint( geopoint );

Backendless.Data.Of( "Friend" ).SetRelation( bob, "coordinates:GeoPoint:1", new object[] { geopoint } );

Friend bob = new Friend();
bob.Name = "Bob" ;
bob.PhoneNumber = "512-555-1212";
bob = Backendless.Data.Of<Friend>().Save( bob );

GeoPoint geopoint = new GeoPoint( 29.76328, -95.36327 ) );
geopoint.Categories.Add( "Home" );
geopoint.Metadata.Add( "description", "Bob's home" );
geopoint = Backendless.Geo.SavePoint( geopoint );

Backendless.Data.Of<Friend>().SetRelation( bob, "Coordinates:GeoPoint:1", new object[]{geopoint} );

 

Run the following query/code to store a data object representing Jane with a link to her home in Houston, TX:

Dictionary<string, object> jane = new Dictionary<string, object>();
jane[ "name" ] = "Jane";
jane[ "phoneNumber" ] = "281-555-1212";
jane = Backendless.Data.Of( "Friend" ).Save( jane );

GeoPoint geopoint = new GeoPoint( 29.76328, -95.36327 );
geopoint.Categories.Add( "Home" );
geopoint.Metadata.Add( "description", "Jane's home" );
geopoint = Backendless.Geo.SavePoint( geopoint );

Backendless.Data.Of( "Friend" ).SetRelation( jane, "coordinates:GeoPoint:1", new object[] { geopoint } );

Friend jane = new Friend();
jane.Name = "Jane" ;
jane.PhoneNumber = "281-555-1212";
jane = Backendless.Data.Of<Friend>().Save( jane );

GeoPoint geopoint = new GeoPoint( 29.76328, -95.36327 ) );
geopoint.Categories.Add( "Home" );
geopoint.Metadata.Add( "description", "Jane's home" );
geopoint = Backendless.Geo.SavePoint( geopoint );

Backendless.Data.Of<Friend>().SetRelation( jane, "Coordinates:GeoPoint:1", new object[]{geopoint} );

 

Run the following query/code to store a data object representing Fred with a link to his home in San Antonio, TX:

Dictionary<string, object> fred = new Dictionary<string, object>();
fred[ "name" ] = "Fred";
fred[ "phoneNumber" ] = "210-555-1212";
fred = Backendless.Data.Of( "Friend" ).Save( fred );

GeoPoint geopoint = new GeoPoint( 29.42412, -98.49363 );
geopoint.Categories.Add( "Home" );
geopoint.Metadata.Add( "description", "Fred's home" );
geopoint = Backendless.Geo.SavePoint( geopoint );

Backendless.Data.Of( "Friend" ).SetRelation( fred, "coordinates:GeoPoint:1", new object[] { geopoint } );
Friend fred = new Friend();
fred.Name = "Fred" ;
fred.PhoneNumber = "210-555-1212";
fred = Backendless.Data.Of<Friend>().Save( fred );

GeoPoint geopoint = new GeoPoint( 29.42412, -98.49363 ) );
geopoint.Categories.Add( "Home" );
geopoint.Metadata.Add( "description", "Fred's home" );
geopoint = Backendless.Geo.SavePoint( geopoint );

Backendless.Data.Of<Friend>().SetRelation( fred, "Coordinates:GeoPoint:1", new object[]{geopoint} );

 

 

Once the data is in the database and the geo location storage, you can verify it in Backendless Console by opening the Geolocation screen and selecting the Home geocategory:

distance-search

 

Suppose you need to get all objects located within 300 miles radius from Beaumont, TX, which has the GPS coordinates of 30.084, -94.145. The following code/query performs that distance-based search:

String whereClause = "distance( 30.26715, -97.74306, " +
                "coordinates.latitude, coordinates.longitude ) < mi(200)";
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause ).SetRelationsDepth( 1 );
IList<Dictionary<string, object>> friends = Backendless.Data.Of( "Friend" ).Find( queryBuilder );

String format = "{0} lives at {1}, {2} tagged as '{3}'";

foreach( Dictionary<string, object> friend in friends )
{
  GeoPoint coordinates = (GeoPoint) friend[ "coordinates" ];
  System.Console.WriteLine( String.Format( format,
    friend[ "name" ],
    coordinates.Latitude,
    coordinates.Longitude,
    (String) coordinates.Metadata[ "description" ] ) );
}
String whereClause = "distance( 30.26715, -97.74306, " +
                "coordinates.latitude, coordinates.longitude ) < mi(200)";
DataQueryBuilder queryBuilder = DataQueryBuilder.Create();
queryBuilder.SetWhereClause( whereClause ).SetRelationsDepth( 1 );
IList<Friend> friends = Backendless.Data.Of<Friend>().Find( queryBuilder );

String format = "{0} lives at {1}, {2} tagged as '{3}'";

foreach( Friend friend in friends )
{
  GeoPoint coordinates = friend.Coordinates;
  System.Console.WriteLine( String.Format( format,
    friend.Name,
    coordinates.Latitude,
    coordinates.Longitude,
    (String) coordinates.Metadata[ "description" ] ) );
}

The search returns all data objects within the specified distance. Each data object has the Coordinates property containing the coordinates of a geo point associated with this data object.

Relations Overview

A data object stored in Backendless may reference other objects from the same or other data tables. These references are called relations. When an object references a single object, it is a one-to-one relation. If a reference is for a collection of objects, it is a one-to-many relation. Relations can be established using either API or Backendless Console. When working with console, a relation in a table must be declared through a relation column. Console allows two types of relation columns: data object relationship and geopoint relationship. The former identifies a relation between two data tables, while the later declares a relationship between a data table and geopoint storage. It is not required to declare relation columns in console - Backendless can derive these relations from the API calls and subsequently creates columns in data tables.

 

The rest of this chapter describes relationship management using Backendless console. For the API reference, see the corresponding chapters.

Declaring a Relation Between Tables

Creating Relations Between Objects in Related Tables

Editing (Update/Delete) Relations

Deleting Relation Column

Declaring a Relation Between Tables

The instructions below describe how to declare a relation between two tables. As an example, the instructions below create a relation column address in the Person table. The new column will reference the Address table:

1. Select the Person table where a relation column should be declared.
2. Click the Schema menu.
3. Click the New button. The pop-up window will display as shown below:
adding-new-column.zoom80
4. Enter column name that will identify the relation.
5. Click the Type drop-down list and select the Data Object Relationship option.
6. Select a related table and the cardinality of the relations from the corresponding drop-down menus. The one-to-one relation means that a table's object can be linked with only one object from the related table. The one-to-many relation means that a table's object can be linked with more than one object from the related table.
relationship-column.zoom80
7. Click CREATE to save the changes.

Once a relationship column is declared, it will appear along other columns in the Data Browser view of the Backendless Console.

Creating Relations Between Objects in Related Tables

Once you have declared a relation between table schemas, you can establish a relation between objects in these tables. Follow the instructions below to link the objects:

1. Click the table name where you declared a relation. Console displays the columns representing relations slightly different than the other ones. The header for these columns includes:
- name of the related table;
- relation type (cardinality) visualized as either a single red line for one-to-one relations or three red lines for one-to-many relations;
- the "auto-load" checkbox.
relation-column-data-browser.zoom70
2. Every object in the table displays a plus icon on mouse hover which can be used to create a relation between that object and one from the related table:
plus-for-relation
3. Click the plus icon for the object to create a relation. The Set Related Object pop-up window will display the list of objects from the related table.
set-relation-popup.zoom70
4. Each object in the displayed popup has either a radio button or a checkbox next to the object's data. Radio buttons are used for one-to-one relations, while checkboxes apply for the one-to-many relations. Select the object(s) which will be linked to the parent object.
5. Click the ADD RELATION button to save the changes.
6. Once a relation between the objects is established, the related object(s) appear as hyperlinks which can be clicked to navigate to them:
relation-created

Editing (Update/Delete) Relations

You can edit the relations between the data objects. Editing a relation allows changing the related object or breaking the relation between the objects.

1. Click the name of the table containing the object with relations. Click the plus icon in the cell displaying the related object (the same plus icon you used to create a relation above):
editing-relation-plus-icon.zoom80
2. The Set Related Object pop-up window will open and display the related object. If you want to link a data object with other object(s), click the radio-button or check-box(s) next to the necessary object(s).
remove-relation.zoom70
3. Depending on the made changes, the action button at the bottom of the popup will say either REMOVE RELATION or UPDATE RELATION. Click the button to finalize the changes.

Deleting Relation Column

When a relation column is deleted, all references between the related objects are also removed. This operation does not delete the actual objects - it removes the column and any references between the related objects.

To delete a relation column using the Backendless Console:

1. Click the name of the table where you want to delete a relation column.
2. Click the Schema menu
3. Click the check-box next to the column you need to delete.
4. Click the Delete menu:
delete-relation-column

Relations API (Set/Add)

This and other relation API sections refer to objects in a relation as parent and child(ren). The parent object is the one that contains a reference to a related object. A child object is one that is being referenced by the parent.

There are two operations supporting creating a relation between objects - setting a relation and adding a relation:

Setting a relation - sets specified child objects as the related objects for the parent. If the parent had other child objects prior to the operation, the relation between them and the parent is removed. This is a replacement operation.
Adding a relation - this operation adds specified child objects to the collection of existing child objects. This is a concatenation operation. For one-to-one relations, if the parent object has a child for the specified column at the time when the operation is called, an error is returned back to the client

 

Other implementation details:

Both parent and child objects must exist in the Backendless storage.
If a column identifies a 1:1 relation which already exists between the parent and some other object, the "set" operation must replace the child object with the one identified in the operation. The "add" operation must return an error.
Add/set methods must return the number of objects added/set, that is, for example, if a whereClause is used and identifies an object (among others) which is already in the relation, that object must not be "accounted for" in the returned number of object.
If add/set receives a non-existing object, it must be ignored and the returned count of objects set/added must not reflect the ignored object.
Both operations create the relation column if it does not exist at the time of the method call.
 

There are two ways to add/set a relation:

Explicit relation list - an API request identifies specific objects to be added/set as children of the parent object. Available APIs:
Set Relation
Add Relation
Implicit relation list - an API request uses a query (the where clause) to identify objects which should be added/set as children of the parent object. Available APIs:
Set Relation
Add Relation

Set Relation with objects

API request replaces existing related objects with the ones identified in the API call. Child objects must be referenced by their objectId values.

Blocking Method:

int result = Backendless.Data.Of( "TABLE-NAME" ).SetRelation( 
                              Dictionary<string, object> parentObject,
                              string relationColumnName,
                              object[] children );
int result = Backendless.Data.Of<E>().SetRelation( 
                              E parentObject,
                              string relationColumnName,
                              object[] children );

Non-blocking Method:

Backendless.Data.Of( "TABLE-NAME" ).SetRelation( 
                 Dictionary<string, object> parentObject,
                 string relationColumnName,
                 object[] children,
                 AsyncCallback<int> callback );
Backendless.Data.Of<E>().SetRelation( 
                 E parentObject,
                 string relationColumnName,
                 object[] children,
                 AsyncCallback;<int> callback );

where:

TABLE-NAME - name of the table where the parent object is stored.
E -  parent's object .NET class. The class name identifies the table where the parent object is stored.
parentObject - the object which will be assigned related children for relatedColumnName. When this argument is an instance of System.Collections.Generic.Dictionary (for the dictionary-based approach), it must contain the objectId property.
relationColumnName - name of the column identifying the relation. Objects from the children array will be set as related for the  column in the parentObject table. The column name may optionally include table name separated by the colon character as well as cardinality which determines the type of relationship (one to one or one to many) (see the note below):

If the column does not exist in the parent table at the time when the API is called, the value of the "relationColumnName" argument must include the name of the child table separated by colon and the cardinality notation. The cardinality is expressed as ":1" for one-to-one relations and ":n" for one-to-many relations. For example, the value of "myOrder:Order:1" will create a one-to-one relation column "myOrder" in the parent table. The column will point to the Order child table. Likewise, the value of "myOrder:Order:n" will create a one-to-many relation column "myOrder" pointing to the Order table.

children - an array of child objects to set into the relation identified by relatedColumnName. For the one-to-one relations the array must contain one element.
callback - a responder object which will receive a callback when the relation has been set or if an error occurs. Applies to the non-blocking method only.

Return Value:

Number of child objects set into the relation. The non-blocking call receives the return value through a callback executed on the AsyncCallback object.

Example:

The example below sets a relation for a one-to-one column named address declared in the Person table. The column must be declared in the table prior to the execution of the request shown below. This necessity is explained by missing table name qualifier in the relationColumnName argument - notice the relation column is "address". If the argument value were "address:Address:1", then the column would be created automatically.
relation-column
 

Dictionary<string, object> parentObject = new Dictionary<string, object>();
parentObject[ "objectId" ] = "41230622-DC4D-204F-FF5A-F893A0324800";

Dictionary<string, object> childObject = new Dictionary<string, object>();
childObject[ "objectId" ] = "3464C734-F5B8-09F1-FFD3-18647D12E700";

object[] children = new object[] { childObject };

AsyncCallback<int> setRelationCallback = new AsyncCallback<int>(
  childrenSet =>
  {
    System.Console.WriteLine( "Number of child objects set in the relation " + childrenSet );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
  
Backendless.Data.Of( "Person" ).SetRelation( parentObject, 
                                             "address", 
                                             children, 
                                             setRelationCallback );
Person personObject = // personObject retrieval is out of scope in this example
Address addressObject = // addressObject retrieval is out of scope in this example

object[] children = new object[] { addressObject };

AsyncCallback<int> setRelationCallback = new AsyncCallback<int>(
  childrenSet =>
  {
    System.Console.WriteLine( "Number of child objects set in the relation " + childrenSet );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
  
Backendless.Data.Of<Person>().SetRelation( personObject, 
                                              "address", 
                                              children, 
                                              setRelationCallback );

Set Relation with condition

API request replaces existing related objects with the ones identified in the API call. Child objects are referenced implicitly - through a whereClause defining the condition for object selection.

Blocking Method:

int result = Backendless.Data.Of( "TABLE-NAME" ).SetRelation( 
                                  Dictionary<string,object> parentObject,
                                  string relationColumnName,
                                  string whereClause );
int result = Backendless.Data.Of<E>().SetRelation( 
                              E parentObject,
                              string relationColumnName,
                              string whereClause );

Non-blocking Method:

Backendless.Data.Of( "TABLE-NAME" ).SetRelation( 
                 Dictionary<string, object> parentObject,
                 string relationColumnName,
                 string whereClause,
                 AsyncCallback<int> callback );
Backendless.Data.Of<E>().SetRelation( 
                 E parentObject,
                 string relationColumnName,
                 string whereClause,
                 AsyncCallback;<int> callback );

where:

TABLE-NAME - Name of the table where the parent object is stored.
E - parent's object .NET class. The class name identifies the table where the parent object is stored.
parentObject - The object which will be assigned related children for relatedColumnName. When this argument is an instance of System.Collections.Generic.Dictionary (for the dictionary-based approach), it must contain the "objectId" property.
relationColumnName - Name of the column identifying the relation. Objects from the children collection will be set as related for the  column in parentObject. The column name may optionally include table name separated by the colon character as well as cardinality which determines the type of relationship (one to one or one to many) (see the note below):

If the column does not exist in the parent table at the time when the API is called, the value of the "relationColumnName" argument must include the name of the child table separated by colon and the cardinality notation. The cardinality is expressed as ":1" for one-to-one relations and ":n" for one-to-many relations. For example, the value of "myOrder:Order:1" will create a one-to-one relation column "myOrder" in the parent table. The column will point to the Order child table. Likewise, the value of "myOrder:Order:n" will create a one-to-many relation column "myOrder" pointing to the Order table.

whereClause - a where clause condition identifying objects in the child table which will be set as the related objects for the parent object.
callback - a responder object which will receive a callback when the relation has been set or if an error occurs. Applies to the non-blocking method only.

Return Value:

Number of child objects set into the relation. The non-blocking call receives the return value through a callback executed on the AsyncCallback object.

Example:

The following request creates a relation between a Person object and all objects in the Users table which match the provided query. The query is specified in the whereClause argument:

name='Joe' or name = 'Frank'.

As a result of the operation, all User objects where the name property is either Joe or Frank will be set in the relation. The relation column is created if it does not exist. This is done because the column contains the child table qualifier, defined as ":Users" right after the column name.

Dictionary<string, object> parentObject = new Dictionary<string, object>();
parentObject[ "objectId" ] = "41230622-DC4D-204F-FF5A-F893A0324800";

AsyncCallback<int> setRelationCallback = new AsyncCallback<int>(
  childrenSet =>
  {
    System.Console.WriteLine( "Number of child objects set in the relation " + childrenSet );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
  
Backendless.Data.Of( "Person" ).SetRelation( parentObject, 
                                             "users:Users:n", 
                                             "name = \"Joe\" or name = \"Frank\"", 
                                             setRelationCallback );
Person personObject = // personObject retrieval is out of scope in this example

AsyncCallback<int> setRelationCallback = new AsyncCallback<int>(
  childrenSet =>
  {
    System.Console.WriteLine( "Number of child objects set in the relation " + childrenSet );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
  
Backendless.Data.Of<Person>().SetRelation( personObject, 
                                 "users:Users:n", 
                                 "name = \"Joe\" or name = \"Frank\"",
                                 setRelationCallback );

Add Relation with objects

This API request adds related objects to the existing collection. Child objects must be referenced by their objectId values.

 

Blocking Method:

int result = Backendless.Data.Of( "TABLE-NAME" ).AddRelation( 
                              Dictionary<string, object> parentObject,
                              string relationColumnName,
                              object[] children );
int result = Backendless.Data.Of<E>().AddRelation( 
                              E parentObject,
                              string relationColumnName,
                              object[] children );

Non-blocking Method:

Backendless.Data.Of( "TABLE-NAME" ).AddRelation( 
                 Dictionary<string, object> parentObject,
                 string relationColumnName,
                 object[] children,
                 AsyncCallback<int> callback );
Backendless.Data.Of<E>().AddRelation( 
                 E parentObject,
                 string relationColumnName,
                 object[] children,
                 AsyncCallback;<int> callback );

where:

TABLE-NAME - Name of the table where the parent object is stored.
E - parent's object .NET class. The class name identifies the table where the parent object is stored.
parentObject - The object which will be assigned related children for relatedColumnName. When this argument is an instance of System.Collections.Generic.Dictionary (for the dictionary-based approach), it must contain the "objectId" property.
relationColumnName - Name of the column identifying the relation. Objects from the children collection will be added as related for the  column in parentObject. The column name may optionally include table name separated by the colon character as well as cardinality which determines the type of relationship (one to one or one to many) (see the note below):

If the column does not exist in the parent table at the time when the API is called, the value of the "relationColumnName" argument must include the name of the child table separated by colon and the cardinality notation. The cardinality is expressed as ":1" for one-to-one relations and ":n" for one-to-many relations. For example, the value of "myOrder:Order:1" will create a one-to-one relation column "myOrder" in the parent table. The column will point to the Order child table. Likewise, the value of "myOrder:Order:n" will create a one-to-many relation column "myOrder" pointing to the Order table.

children - An array of child objects to add to the relation identified by relatedColumnName.
callback - a responder object which will receive a callback when the related objects have been added or if an error occurs. Applies to the non-blocking method only.

Return Value:

Number of child objects added to the relation. The non-blocking call receives the return value through a callback executed on the AsyncCallback object.

Example:

The example below adds a related object for a one-to-one column named address declared in the Person table. If the relation already contains an object, server returns an error since the addRelation API for one-to-one relations can add an object only once. The column must be declared in the table prior to the execution of the request shown below. This necessity is explained by missing table name qualifier in the relationColumnName argument - notice the relation column name is "address". If it were specified as "address:Address", then the column would be created automatically.
relation-column
 

Dictionary<string, object> parentObject = new Dictionary<string, object>();
parentObject[ "objectId" ] = "41230622-DC4D-204F-FF5A-F893A0324800";

Dictionary<string, object> childObject = new Dictionary<string, object>();
childObject[ "objectId" ] = "3464C734-F5B8-09F1-FFD3-18647D12E700";

object[] children = new object[] { childObject };

AsyncCallback<int> addRelationCallback = new AsyncCallback<int>(
  childrenSet =>
  {
    System.Console.WriteLine( "Number of child objects added in the relation " + childrenSet );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
  
Backendless.Data.Of( "Person" ).AddRelation( parentObject, 
                                             "address:Address:n", 
                                             children, 
                                             addRelationCallback );
Person personObject = // personObject retrieval is out of scope in this example
Address addressObject = // addressObject retrieval is out of scope in this example

object[] children = new object[] { addressObject };

AsyncCallback<int> addRelationCallback = new AsyncCallback<int>(
  childrenSet =>
  {
    System.Console.WriteLine( "Number of child objects added in the relation " + childrenSet );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
  
Backendless.Data.Of<Person>().AddRelation( personObject, 
                                              "address:Address:n", 
                                              children, 
                                              addRelationCallback );

Add Relation with condition

This API request adds related objects to the existing collection. Child objects to add to the relation are defined through a whereClause condition.

Blocking Method:

int result = Backendless.Data.Of( "TABLE-NAME" ).AddRelation( 
                              Dictionary<string, object> parentObject,
                              string relationColumnName,
                              string whereClause );
int result = Backendless.Data.Of<E>().AddRelation( 
                              E parentObject,
                              string relationColumnName,
                              string whereClause );

Non-blocking Method:

Backendless.Data.Of( "TABLE-NAME" ).AddRelation( 
                 Dictionary<string, object> parentObject,
                 string relationColumnName,
                 string whereClause,
                 AsyncCallback<int> callback );
Backendless.Data.Of<E>().AddRelation( 
                 E parentObject,
                 string relationColumnName,
                 string whereClause,
                 AsyncCallback;<int> callback );

where:

TABLE-NAME - Name of the table where the parent object is stored.
E - parent's object .NET class. The class name identifies the table where the parent object is stored.
parentObject - The object which will be assigned related children for relatedColumnName. When this argument is an instance of System.Collections.Generic.Dictionary (for the dictionary-based approach), it must contain the "objectId" property.
relationColumnName - Name of the column identifying the relation. Objects from the children collection will be added as related for the  column in parentObject. The column name may optionally include table name separated by the colon character as well as cardinality which determines the type of relationship (one to one or one to many) (see the note below):

If the column does not exist in the parent table at the time when the API is called, the value of the "relationColumnName" argument must include the name of the child table separated by colon and the cardinality notation. The cardinality is expressed as ":1" for one-to-one relations and ":n" for one-to-many relations. For example, the value of "myOrder:Order:1" will create a one-to-one relation column "myOrder" in the parent table. The column will point to the Order child table. Likewise, the value of "myOrder:Order:n" will create a one-to-many relation column "myOrder" pointing to the Order table.

whereClause - a where clause condition identifying objects in the child table which will be added as related objects to the parent object.
callback - a responder object which will receive a callback when the related objects have been added or if an error occurs. Applies to the non-blocking method only.

Return Value:

Number of child objects added to the relation. The blocking call receives the return value through a callback executed on the AsyncCallback object.

Example:

The following request adds objects from the Users table to a related property/column the Person object. Child objects added to the relation  must match the provided query. The query is specified in the whereClause argument:

name='Joe' or name = 'Frank'.

As a result of the operation, all User objects where the name property is either Joe or Frank will be added to the relation. The relation column is created if it does not exist. This is done because the column contains the child table qualifier, defined as ":Users" right after the column name.

Dictionary<string, object> parentObject = new Dictionary<string, object>();
parentObject[ "objectId" ] = "41230622-DC4D-204F-FF5A-F893A0324800";

AsyncCallback<int> addRelationCallback = new AsyncCallback<int>(
  childrenSet =>
  {
    System.Console.WriteLine( "Number of child objects added in the relation " + childrenSet );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
  
Backendless.Data.Of( "Person" ).AddRelation( parentObject, 
                                             "users:Users:n", 
                                             "name = \"Joe\" or name = \"Frank\"", 
                                             addRelationCallback );
Person personObject = // personObject retrieval is out of scope in this example

AsyncCallback<int> addRelationCallback = new AsyncCallback<int>(
  childrenSet =>
  {
    System.Console.WriteLine( "Number of child objects added in the relation " + childrenSet );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
  
Backendless.Data.Of<Person>().AddRelation( personObject, 
                                              "users:Users:n", 
                                              "name = \"Joe\" or name = \"Frank\"",
                                              addRelationCallback );

Relations API (Delete)

This and other relation API sections refer to objects in a relation as parent and child(ren). The parent object is the one that contains a reference to a related object. A child object is one that is being referenced by the parent.

Relation deletion API disconnects a relation between the parent object and some (or all) related children. The API does not delete any objects, it is responsible strictly for breaking the relationship. For object deletion API, see the Deleting Data Objects section of the documentation.

 

There are two ways to delete a relation:

Explicit deletion - an API request identifies specific child objects to be disassociated from a relationship with the parent object.

Implicit deletion - an API request uses a condition (the where clause) to identify the child objects which should be disconnected from the parent object.

Delete Objects from relation

The API removes specific objects from a relationship with their parent.

Blocking Method:

int result = Backendless.Data.Of( "TABLE-NAME" ).DeleteRelation( 
                              Dictionary<string, object> parentObject,
                              string relationColumnName,
                              object[] children );
int result = Backendless.Data.Of<E>().DeleteRelation( 
                            E parentObject,
                            string relationColumnName,
                            object[] children );

Non-blocking Method:

Backendless.Data.Of( "TABLE-NAME" ).DeleteRelation( 
                 Dictionary<string, object> parentObject,
                 string relationColumnName,
                 object[] children,
                 AsyncCallback<int> callback );
Backendless.Data.Of<E>().DeleteRelation( 
                 E parentObject,
                 string relationColumnName,
                 object[] children,
                 AsyncCallback;<int> callback );

where:

TABLE-NAME - Name of the table where the parent object is stored.
E - .NET class of the parent object. The class name identifies the table where the parent object is stored.
parentObject - The object for which the relation with the specified children will be deleted. When this argument is an instance of Dictionary<string, object> (for the dictionary-based approach), it must contain the "objectId" property.
relationColumnName - Name of the column identifying the relation. Relationship between the specified objects from the children collection will be deleted for the column in parentObject.
children - An array of child objects for which the relationship with the parentObject will be deleted. Each object must have a valid value for the objectId property.
callback - a responder object which will receive a callback when the relation has been deleted or if an error occurs. Applies to the asynchronous method only.

Return Value:

Number of child objects for which the relationship has been deleted. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Example:

The example below deletes a relation between a Person object and its children. The child objects are referenced explicitly in the API call.The relation column is address.

Dictionary<string, object> parentObject = new Dictionary<string, object>();
parentObject[ "objectId" ] = "41230622-DC4D-204F-FF5A-F893A0324800";

Dictionary<string, object> childObject1 = new Dictionary<string, object>();
childObject1[ "objectId" ] = "XXXX-XXXX-XXXX-XXXXX";

Dictionary<string, object> childObject2 = new Dictionary<string, object>();
childObject2[ "objectId" ] = "ZZZZ-ZZZZ-ZZZZZ-ZZZZZ";

object[] children = new object[] { childObject1, childObject2 };

AsyncCallback<int> deleteRelationCallback = new AsyncCallback<int>(
  howMany =>
  {
    System.Console.WriteLine( String.Format( "Server has removed {0} objects from the relation", howMany ) );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );

Backendless.Data.Of( "Person" ).DeleteRelation( 
                  parentObject, 
                  "address", 
                  children, 
                  deleteRelationCallback );
Person personObject = // personObject retrieval is out of scope in this example
Address addressObject1 = // addressObject retrieval is out of scope in this example
Address addressObject2 = // addressObject retrieval is out of scope in this example
object[] childObjects = new object[] { addressObject1, addressObject2 };

AsyncCallback<int> deleteRelationCallback = new AsyncCallback<int>(
  howMany =>
  {
    System.Console.WriteLine( String.Format( "Server has removed {0} objects from the relation", howMany ) );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
  
Backendless.Data.Of<Person>().DeleteRelation( 
                      personObject, 
                      "address", 
                      childObjects,
                      deleteRelationCallback );

Delete Relation using condition

The API deletes objects from a relationship with their parent. The objects are identified implicitly through a whereClause condition.

Blocking Method:

int result = Backendless.Data.Of( "TABLE-NAME" ).DeleteRelation( 
                               Dictionary<string, object> parentObject,
                               string relationColumnName,
                               string whereClause );
int result = Backendless.Data.Of<E>().DeleteRelation( 
                              E parentObject,
                              string relationColumnName,
                              string whereClause );

Non-blocking Method:

Backendless.Data.Of( "TABLE-NAME" ).DeleteRelation( 
                  Dictionary<string, object> parentObject,
                  string relationColumnName,
                  string whereClause,
                  AsyncCallback<int> callback );
Backendless.Data.Of<E>().DeleteRelation( 
                 E parentObject,
                 string relationColumnName,
                 string whereClause,
                 AsyncCallback;<int> callback );

where:

TABLE-NAME - Name of the table where the parent object is stored.
E - .NET class of the parent object. The class name identifies the table where the parent object is stored.
parentObject - The object for which the relation with the identified children will be deleted. When this argument is an instance of Dictionary<string, object> (for the dictionary-based approach), it must contain the "objectId" property.
relationColumnName - Name of the column representing the relation. Relationship between the child objects identified by whereClause will be deleted for this column in parentObject.
whereClause - a where clause condition identifying the objects in the child table which will be removed from the relation to the parent object.
callback - a responder object which will receive a callback when the relation has been deleted or if an error occurs. Applies to the asynchronous method only.

Return Value:

Number of child objects removed from the relationship. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Example:

The following example deletes a relation between a Person object and all the related objects in the related table identified by column "user" which match the provided query:

name='Joe' or name = 'Frank'

As a result of the operation, all related objects where the name property is either Joe or Frank will be deleted from the relation.

Dictionary<string, object> parentObject = new Dictionary<string, object>();
parentObject[ "objectId" ] = "41230622-DC4D-204F-FF5A-F893A0324800";

AsyncCallback<int> deleteRelationCallback = new AsyncCallback<int>(
  howMany =>
  {
    System.Console.WriteLine( String.Format( "Server has removed {0} objects from the relation", howMany ) );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
  
Backendless.Data.Of( "Person" ).DeleteRelation( parentObject, 
                  "user", 
                  "name = \"Joe\" or name = \"Frank\"", 
                  deleteRelationCallback );
Person personObject = // personObject retrieval is out of scope in this example

AsyncCallback<int> deleteRelationCallback = new AsyncCallback<int>(
  howMany =>
  {
    System.Console.WriteLine( String.Format( "Server has removed {0} objects from the relation", howMany ) );
  },
  error =>
  {
    System.Console.WriteLine( "Server returned an error " + error.Message );
  } );
  
Backendless.Data.Of<Person>().DeleteRelation( 
                  personObject, 
                  "user", 
                  "name = \"Joe\" or name = \"Frank\"",
                  deleteRelationCallback );

Relations (Retrieve)

Relations API (Retrieve) Overview

When a data object or a collection of objects is retrieved using the client API, Backendless does not automatically include related objects into the returned object hierarchy. The reason for this is it would make it very easy to load a lot of unnecessary data which could impact application's performance. There are multiple ways to retrieve related data:

Relations auto-load - a mechanism built-into Backendless Console for configuring specific relations to be included into responses with the parent objects.
Single-step relations retrieval - initializing and including related objects into the response when running a find query for parent objects.
Two-step relations retrieval - a process of retrieving relations where the first step is to load the parent object and the second step is to load specific relations for the given parent.
Loading with relations depth - retrieving related objects by specifying the depth of the object hierarchy
Relation paging API - enables "chunked" (or paged) retrieval of related objects
Inverted Retrieval - loading related child objects by condition applied to parent - load related objects using a search query where the condition applies to the parent object properties.

Auto Load

The auto-load feature is designed to be used for quick prototyping. One of its benefits is reduction of client-side code. However, for production quality apps, it is not recommended to use the auto-load relation retrieval as it can greatly impact performance.

By default when an object is retrieved from Backendless using any of the find APIs (basic or advanced), its related objects are not included into the response, unless explicitly referenced in the request. This behavior can be easily modified using Backendless Console:

auto-load

For any two tables A and B where A has a relationship column linking it to B, the console includes the "auto load" checkbox for the relationship column. Selecting the checkbox instructs Backendless to return a paged set of related B objects when the parent instance of A is retrieved through an API call. For example, in the image above, the shown table has a one-to-many "address" relationship with the Address table. When the "auto load" checkbox in the "address" column is selected, a paged set of the related Address objects will be included into the response for a find query for the shown table.

You can disable the auto-load option by setting the relation depth parameter to 0 in an API call.

Single Step Retrieval

Single Step Retrieval loads only a partial set of the related objects (default size of the retrieved related collection is 10). To load additional related objects, use the Relation Paging API.

This approach allows retrieval of a partial set of the related objects along with the parent object in a single find or findById request. Each relationship property (column) must be uniquely identified by name using the API documented below.

Retrieving a specific object with relations:

Non-blocking call:
Backendless.Data.Of( "TABLE-NAME" ).FindById( 
                  string objectId, 
                  IList<string> relations,
                  AsyncCallback<Dictionary<string, object>> responder )
Blocking call:
Dictionary<string, object> result = Backendless.Data.Of( "TABLE-NAME" ).FindById( 
                  string objectId, 
                  IList<string> relations )
Non-blocking call:
Backendless.Data.Of<E>().FindById( 
                  string objectId, 
                  IList<string> relations,
                  AsyncCallback<E> responder )
Blocking call:
E result = Backendless.Data.Of( "TABLE-NAME" ).FindById( 
                  string objectId, 
                  IList<string> relations )

 

Retrieving a collection of objects with relations:

DataQueryBuilder queryBuilder = Backendless.DataQueryBuilder.Create();
queryBuilder.AddRelated( "RELATED-PROPERTY-NAME" );
queryBuilder.AddRelated( "RELATED-PROPERTY-NAME.RELATION-OF-RELATION" );

Then load data using the constructed queryBuilder object with:

Non-blocking call:
Backendless.Data.Of( "TABLE-NAME" ).Find( 
                  DataQueryBuilder queryBuilder, 
                  AsyncCallback<IList<Dictionary<string, object>>> callback  )
Blocking call:
IList<Dictionary<string, object>> result = Backendless.Data.Of( "TABLE-NAME" ).Find( 
                                                               DataQueryBuilder queryBuilder )
Non-blocking call:
Backendless.Data.Of<E>().Find( 
                  DataQueryBuilder queryBuilder, 
                  AsyncCallback<IList<E>> callback  )
Blocking call:
IList<E> result = Backendless.Data.Of<E>().Find( DataQueryBuilder queryBuilder )

where

RELATED-PROPERTY-NAME - name of a related property to load. For example, if table Person has a relation "homeAddress" pointing to an object in the Address table, the value would be "homeAddress". The syntax allows to add relations of relations. For example, if the same Address table has a relation "country" pointing to the Country table, then "homeAddress.country" would instruct the related Country object to be loaded as well.
queryBuilder - an instance of the DataQueryBuilder class. The class is used to identify related properties for relation retrieval.
TABLE-NAME - Name of the table where the data should be retrieved from.
E - Reference to a class which identifies a table where the data should be loaded from.

Two-step Retrieval

Two Step Retrieval loads only a partial set of the related objects (default size of the retrieved related collection is 10). To load additional related objects, use the Relation Paging API.

With this approach a collection of related objects for a specific relation property in a parent object is retrieved from the server. The client application must know parent object's objectId. The API loads a collection of related children for one property at a time. Child objects retrieved in paged sets, see the Relation Paging API sectionfor additional details.

 

Suppose the Person table has a one-to-many relationship column friends pointing to the Users table. The code below retrieves related BackendlessUser objects  for a specific Person object:

Prepare LoadRelationsQueryBuilder:
LoadRelationsQueryBuilder<Map<String, Object>> loadRelationsQueryBuilder; 
loadRelationsQueryBuilder = LoadRelationsQueryBuilder.ofMap();
loadRelationsQueryBuilder.setRelationName( "friends" );
Synchronous call:
String parentObjectId = // removed for brevity
List<Map<String, Object>> friends;
friends = Backendless.Data.of( "Person" ).loadRelations( parentObjectId, 
                                                         loadRelationsQueryBuilder );
for( Map friend: friends )
  Log.i( "MYAPP", friend.get( "email" ));
Asynchronous call:
String parentObjectId = // removed for brevity
Backendless.Data.of( "Person" ).loadRelations( parentObjectId,
        loadRelationsQueryBuilder,
        new AsyncCallback<List<Map<String, Object>>>()
        {
          @Override
          public void handleResponse( List<Map<String, Object>> friends )
          {
             for( Map friend: friends )
               Log.i( "MYAPP", friend.get( "email" ));
          }

          @Override
          public void handleFault( BackendlessFault fault )
          {
             Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
          }
        } );
Prepare LoadRelationsQueryBuilder:
LoadRelationsQueryBuilder<BackendlessUser> loadRelationsQueryBuilder; 
loadRelationsQueryBuilder = LoadRelationsQueryBuilder.of( BackendlessUser.class );
loadRelationsQueryBuilder.setRelationName( "friends" );
Synchronous call:
String parentObjectId = // removed for brevity
List<BackendlessUser> friends;
friends = Backendless.Data.of( Person.class ).loadRelations( objectId, 
                                                             loadRelationsQueryBuilder );
for( BackendlessUser friend: friends )
  Log.i( "MYAPP", friend.getEmail() );
Asynchronous call:
String parentObjectId = // removed for brevity
Backendless.Data.of( "Person" ).loadRelations( objectId,
        loadRelationsQueryBuilder,
        new AsyncCallback<List<BackendlessUser>>()
        {
          @Override
          public void handleResponse( List<BackendlessUser> friends )
          {
             for( BackendlessUser friend: friends )
               Log.i( "MYAPP", friend.getEmail() );
          }

          @Override
          public void handleFault( BackendlessFault fault )
          {
             Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
          }
        } );

Retrieval with Relation Depth

The Data Service API supports a mechanism for loading related objects without identifying each by its name. Instead, the API includes a parameter which specifies the "depth" of the relations to include into the response. Consider the following diagram:

relation-depth

The diagram shows a hierarchy for class structure - the Order class has two relations: with OrderItem and Customer classes. Each in turn has a relation to the Manufacturer and Address classes. When an instance or a collection of Order objects is retrieved from Backendless, the API may include a parameter specifying the depth of relations to include into the response. If the relation depth is 1, then all related instances of OrderItem and Customer will be included into each Order object. If the relation depth is 2, then not only OrderItem and Customer instances will be included, but the corresponding Manufacturer and Address objects as well.

Loading relations with relation depth retrieves only a partial set of the related objects (default size of the retrieved related collection is 10). To load additional related objects, use the Relation Paging API.

API methods supporting relations depth

All methods are available via Backendless.Persistence accessor, for example Backendless.Persistence.FindFirst( 2 ):

Synchronous methods:

public T FindFirst( int relationsDepth );
public T FindLast( int relationsDepth );
public T FindById( string id, int relationsDepth );
public BackendlessCollection<T> Find( BackendlessDataQuery dataQueryOptions );

Asynchronous methods:

public void FindFirst( int relationsDepth, AsyncCallback<T> responder );
public void FindLast( int relationsDepth, AsyncCallback<T> responder );
public void FindById( string id, int relationsDepth, AsyncCallback<T> responder );
public void Find( BackendlessDataQuery dataQueryOptions, AsyncCallback<BackendlessCollection<T>> responder );

Example:

BackendlessDataQuery query = new BackendlessDataQuery();
QueryOptions queryOptions = new QueryOptions();
queryOptions.RelationsDepth = 2;
query.QueryOptions = queryOptions;
BackendlessCollection<Foo> invoices = Backendless.Persistence.Of<Foo>().Find( query );

where

Foo - reference to a class which identifies the table from which the data is to be loaded.

Relation Paging

API operations described above provide a way to retrieve a limited (initial) set of related data. In most real world scenarios, there may be more related objects than returned by Single Step, Two Step or Relation Depth Retrieval APIs. To obtain a complete set of related objects Backendless supports Relation Paging API described below.

1. To understand the concept of paged data retrieval, see the Data retrieval with Paging section of the documentation.
2. When working with paging, it may be important to know the total size of the related collection. See the Get Object Count section for details on how to get the related objects count.

 

Inverted Relation Retrieval

Backendless supports a special query syntax for loading a subset of child objects for a specific parent. Consider the following table schemas:

 

PhoneBook table:

phonebook-table-schema

 

Contact table:

contact-table-schema

 

Address table:

address-table-schema

These tables will be used to demonstrate how to query Backendless for conditional related object retrieval.

 

The general structure of a whereClause query to load a collection of child objects for a specific parent object is:

ParentTableName[ relatedColumnName ].parentColumnName COLUMN-VALUE-CONDITION

Both columns relatedColumnName and parentColumnName must be declared in a table with name of ParentTableName. The relatedColumnName must be a relation column. The table relatedColumnName points to is the table where the objects must be loaded from.

Relations with Geo Points

Backendless Geo Service manages application's geo location data and provides APIs to work with Geo points. Backendless supports integration between data objects managed by Data Service and geo points for the scenarios when a logical connection between the two entity types must exist in an application. For instance, in a taxi ordering application a data object may represent a taxi car, while a geo point represents its location on the map. Linking the two entity types together provides great benefits such as retrieving both objects at once and managing as a consistent, cohesive object hierarchy.

The Data-to-Geo integration is implemented through object relations. Data table schema may declare a table column with a special data type - "GeoPoint Relationship". As a result, the data objects in the table may contain a reference to one or more GeoPoints. When a data object with a related GeoPoint is saved, Backendless persists information about both the data object and the geo point in the corresponding persistent systems and sets up the relationship. Likewise, when a data object is retrieved by using the API, any related geo points can be retrieved using the same principle for loading data relations. The data-to-geo relation is bidirectional, it means a geo point may reference a data object in its metadata. You can learn more about it in the Relations with Data Objects section of the Geolocation documentation.

The relationship between a data object and a geo point (or a collection of) can be established by using either the "code first" or the "schema first" approaches. With the former, the relationship is determined by the data structure persisted with the API. If a data object references a GeoPoint (or a collection of) in one of its properties, Backendless interprets it as a relation and, as a result, will create a relation column in the data table schema. With the latter ("schema first") approach, application developer can declare a relationship in the data table schema first. In either one of these approaches, once a relationship is declared, data objects and geo points may be linked together by using the Backendless console as well.

 

This chapter consists of the following sections:

Declaring a Data-to-Geo Relationship in Table Schema

Linking a Data Object with Geo Points

Update/Delete Relations

Deleting Relation Column in Table Schema

Establishing Relations with Geo Points via API

Declaring a Data-to-Geo Relationship in Table Schema


To declare a relationship in a data table schema:

1. Select a table where a relation column should be declared.
2. Click the Schema menu.
3. Click the New button. Enter a name for the new column in the Name field. This column will represent a data-to-geo relationship.
4. Select the Geopoint Relationship option from the Type drop-down list.
geo-point-relationship-type
5. Select the constraints and/or cardinality of the relation from the corresponding drop-down menus.
 
Constraints:
There are two available constraints: "Not Null (Required)" and "Unique Value". The former (NN) establishes the column as required. It means when a new object is created or an existing one is updated, server will be expecting a value for the column. The latter constraint (UQ) will enforce a rule that the column must contain a unique value, thus no two objects will be able to contain the same GeoPoint.
 
Cardinality:
The one-to-one relation means that a table's object can be linked with only one geo point, while the one-to-many relation means that a table's object can be linked with multiple geo points.
location-column
6. Click the CREATE button to save the changes.

Linking a Data Object with Geo Points


Once a data-to-geo relationship column is declared, data objects from the table can be linked to geo point(s) as described below:

1. Click the name of the table containing an object you want to link with a geo point.
2. Table columns representing the data-to-geo relationships are identified as "GEOPOINT relationship" in the header row. The cardinality of the relation is visualized as one red line for the one-to-one relations and three red lines for the one-to-many relations:
geopoint-column.zoom70
3. To create a relationship between an object and a geopoint, click the plus icon next in the cell for a GEOPOINT column.
plus-icon-for-geopoint.zoom70
4. The Set Related GeoPoint pop-up window will display the list of the geo points. Use the Geo Category drop-down list to select a geo category from which the points should be displayed. Additionally, you can use the search bar to locate a geopoint by its metadata:
geopoint-search.zoom80
5. If you declared a one-to-one relation for a table the object belongs to, you will be able to link this object with only one geo point (by the means of a radio button). If it is a one-to-many relationship, the interface uses check boxes, which allow for multiple selection. Click a radio-button or select check-boxes next to the geo points which you want to link with the data object.
6. Click the ADD RELATION button to save the changes.

Once a relation is established, it is shown in the data browser as a hyperlink. The hyperlink for the one-to-one relations displays the coordinates of the related geo point. For the one-to-many relations the link says "multiple Geopoints". In both cases, the link opens the Geolocation screen of the console which displays the related geo point(s). Additionally, the link has a mouse-over preview for the related geopoints:

geopoint-preview

Update/Delete Relations


To update a data-to-geo relation, use the same plus icon that opened the popup to create a geopoint relation. The popup tracks the selection changes and allows update or deletion of the relation.

Deleting Relation Column in Table Schema


A data-to-geo relationship can be removed at the schema level. When a relationship column is removed, the "links" between the corresponding data objects and geopoints are deleted, however, it does not remove the objects themselves.

To delete a relationship definition between a data table and the geo points:

1. Click the name of the table which contains a "GeoPoint relationship" column you need to remove.
2. Click the Schema menu
3. Click the check-box next to the column you need to delete.
4. Click the Delete menu.
delete-geopoint-relation-column

Establishing Relations with Geo Points via API


Creating a relationship between a data object and a geo point (or a collection of) uses the same API as saving a data object with a related entity. In the case of data-to-geo relations, the related entity is a geopoint or a collection of geopoints. Consider the example that below saves a data object with a related geopoint. The geopoint is also persisted in the Geo Service:

 

The example below demonstrates how to link a taxi (a data object) with its location (geo point). First, declare the TaxiCab class:

class TaxiCab 
  { 
    public String CarMake; 
    public String CarModel; 
    public GeoPoint Location { get; set; } 
    public List<GeoPoint> PreviousDropOffs { get; set; } 
  }

To set one-to-one or one-to-may relation:

TaxiCab taxi = new TaxiCab(); 
      taxi.CarMake = "Toyota"; 
      taxi.CarModel = "Prius"; 

      // one-to-one relation between a data object and a geo point 
      taxi.Location = new GeoPoint( 40.7148, -74.0059 ); 
      taxi.Location.Categories.Add( "taxi" ); 
      taxi.Location.Metadata.Add( "service_area", "NYC" ); 

      // one-to-many relation between a data object and geo points 
      List<GeoPoint> previousDropOffs = new List<GeoPoint>(); 

      GeoPoint droppOff1 = new GeoPoint( 40.757977, -73.98557 ); 
      droppOff1.Metadata.Add( "name", "Times Square" ); 
      droppOff1.Categories.Add( "DropOffs" ); 
      previousDropOffs.Add( droppOff1 ); 

      GeoPoint droppOff2 = new GeoPoint( 40.748379, -73.985565 ); 
      droppOff2.Metadata.Add( "name", "Empire State Building" ); 
      droppOff2.Categories.Add( "DropOffs" ); 
      previousDropOffs.Add( droppOff2 ); 

      taxi.PreviousDropOffs = previousDropOffs; 

      Backendless.Data.Of<TaxiCab>().Save( taxi );

After you run the code, you will see the following data object and geopoints created in the application:

Data object:

taxi-data-object.zoom70        

Geopoints:

taxi-geo-points.zoom70

Data Security

Data Service supports a very flexible security mechanism for restricting access to objects stored in Backendless. Security permissions apply to users and roles. A permission can either grant or reject an operation for a particular asset. In the context of Data Service, the asset is an object which your app can retrieve, update or delete. Permissions can be granted or rejected globally, where they apply to all tables and all objects in the data store. Additionally, every table may have its own permission matrix and owner policy - a special instruction whether object owners can or cannot retrieve/update/delete the objects they 'own'. Finally, every object has its own Access Control List (ACL) which is a matrix of permissions for the operations applicable specifically to the object:

backendless-security-components

The security system is multi-layered. For any API call the system goes through several layers where each can trim the scope of the operations. The layered order of the decision making is important and consists of the following points of validation:

1. ObjectACL for the user who makes the call
2. ObjectACL for user-defined roles assigned to the user who makes the call.
3. Table permissions for the User account
4. Table permissions for the user-defined roles
5. Owner Policy
6. ObjectACL for system roles
7. Table permissions for system-level roles
8. Global user-defined roles
9. Global system roles

Where:

"User-defined roles" - roles created by the application developer.
"System roles" - roles built into Backendless (AuthenticatedUser, NonAuthenticatedUser, SocialUser, ServerCodeUser, etc).

 

Consider the following guide which illustrates the decision making process:

Backend receives an API request to load data from a table (the Find operation). For the sake of the example, user's identity is "bob@backendless.com" and the user belongs to a custom role called "MyRole".
 
All objects in the table become candidates for the retrieval. Backendless goes through the security permissions layers to determine which objects must be included into the response.
 

1. [LAYER 1] ObjectACL for the user who makes the call.
Backendless checks if there are any restrictions for the user account at the object level. Any object in the collection with ACL which rejects access to the user is excluded from the result. To see or modify the permissions for a particular object, click the 'lock' icon in the ACL column in the data browser in management console to see and manage Object ACL permissions for a specific object.
object-acl-access.zoom80
 
2. [LAYER 2] ObjectACL for user-defined roles assigned to the user who makes the call.
This is the same check as the one above, except Backendless looks into the permissions for the roles defined by the application developer. If the user who made the call belongs to any of the custom roles, Backendless checks if these roles are allowed to perform the current operation for every object in the collection. In the screenshot below, only the "MyRole" role will be checked in this step, since this is the only custom role in the application:
object-acl-custom-role.zoom80
 
3. [LAYER 3] Table permissions for the User account.
Every table in Backendless may have its own set of permissions for users and roles. At this point Backendless checks if the currently logged in user is allowed to run the current operation. For example, if the Find operation is denied for the user, no objects would be returned. To see the permissions for a user account in Backendless Console, select a table and click PERMISSIONS, then USER PERMISSIONS menu:
table-permissions-for-users.zoom80

 

4. [LAYER 4] Table permissions for the user-defined roles.
This step is identical to the one described above with the exception that is checks custom roles for the table. Since this guide reviews the decision making process for the Find operation, Backendless checks the column for Find. If any of the custom roles which the user belongs to deny access, the operation is rejected and no data is returned.
custom-user-roles-for-table.zoom80
 
5. [LAYER 5] Owner Policy.
When a new object is created in Backendless, the system automatically links it with the account of the user that made the call to save the object. You can see that information in the 'ownerId' column in any of your tables in the data browser.  With the association between objects and users (owners), Backendless provides a way to control whether users can get access to the data they created. This is done through a concept we call 'Owner Policy'. To navigate to Owner Policy, select a table in the data browser and click the PERMISSIONS menu. Click the OWNER POLICY menu item.
Granting a permission for an operation in Owner Policy, guarantees that the objects owned by the current user will be allowed access for the specified operations. Denying a permission, takes out the 'owned' objects from the collection of candidate objects to return.
 
6. [LAYER 6] Object ACL for system roles.
This check is identical to step 2 ([LAYER 2] Object ACL for custom roles). The difference is the system roles cover larger groups of users. For example, this step would make possible to restrict access to specific objects for all authenticated (or not authenticated) users, yet the object would be returned with a query made by the object's owner if the Owner Policy (previous step) grants access.  An exception to this rule are API calls from business logic. In that case, if there is no authenticated user in the context of the call, Backendless assigns only ServerCodeUser role. The NotAuthenticatedUser role is not assigned and thus is not checked.
 
7. [LAYER 7] Table permissions for system roles.
Identical to step 4 (table permissions for custom roles), this checks if any of the system roles reject the operation at the table level.
 
8. [LAYER 8] Global custom roles.
Global policy applies to all tables and objects. By default all table level permissions inherit from the global policy. You can configure in the console at: Users > Security Roles. Create a new role and click it to configure the permission matrix:
custom-role-global-matrix.zoom70

Permissions API

Every data object in Backendless has its own access control list (ACL) - a matrix of operations and principals (application's users or roles). An intersection of an operation and a principal contains a permission which determines whether the principal has the right to execute the operation. These permission could be either grant or deny. Backendless console provides an easy to understand way to see and manage these permissions. For example, the screenshot below demonstrates an ACL matrix for an object. Notice the intersection of a column for the Create operation and the AuthenticatedUser role. The cell contains a green checkmark icon representing that the permission is granted:

permission-matrix

In addition to managing the ACL permissions with Backendless Console there is also Permissions API:

Method Signatures

The following method signatures are used for granting or denying access to a data object for a user, a role, all users, or all roles:

public void GrantForUser<T>( String userId, T dataObject )
public void GrantForUser<T>( String userId, T dataObject, AsyncCallback<Object> responder )
public void DenyForUser<T>( String userId, T dataObject )
public void DenyForUser<T>( String userId, T dataObject, AsyncCallback<Object> responder )
public void GrantForRole<T>( String roleName, T dataObject )
public void GrantForRole<T>( String roleName, T dataObject, AsyncCallback<Object> responder )
public void DenyForRole<T>( String roleName, T dataObject )
public void DenyForRole<T>( String roleName, T dataObject, AsyncCallback<Object> responder )
public void GrantForAllUsers<T>( T dataObject )
public void GrantForAllUsers<T>( T dataObject, AsyncCallback<Object> responder )
public void DenyForAllUsers<T>( Object dataObject )
public void DenyForAllUsers<T>( T dataObject, AsyncCallback<Object> responder )
public void GrantForAllRoles<T>( T dataObject )
public void GrantForAllRoles<T>( T dataObject, AsyncCallback<Object> responder )
public void DenyForAllRoles<T>( T dataObject )
public void DenyForAllRoles<T>( T dataObject, AsyncCallback<Object> responder )

Use these methods for the Data operations (that is, to find, update, and remove the data) performed by DataPermission class, which is available in BackendlessAPI.Persistence namespace.

DataPermission.FIND.<method> to set a permission to find/retrieve a data object
DataPermission.UPDATE.<method> to set a permission to update a data object
DataPermission.REMOVE.<method> to set a permission to remove a data object

To grant access for a user

DataPermission.FIND.GrantForUser( userId, dataObject );
DataPermission.UPDATE.GrantForUser( userId, dataObject );
DataPermission.REMOVE.GrantForUser( userId, dataObject );

where:

userId - ID of a user, for which you want to grant the find, update, or remove  permission.
dataObject - an object for which you want to grant a permission.

To grant access for a user role

DataPermission.FIND.GrantForRole( roleName, dataObject );
DataPermission.UPDATE.GrantForRole( roleName, dataObject );
DataPermission.REMOVE.GrantForRole( roleName, dataObject );

where:

roleName - name of a user role, for which you want to grant the find, update, or remove  permission.
dataObject - an object for which you want to grant a permission.

To grant access for all users

DataPermission.FIND.GrantForAllUsers( dataObject );
DataPermission.UPDATE.GrantForAllUsers( dataObject );
DataPermission.REMOVE.GrantForAllUsers( dataObject );

where:

dataObject - an object for which you want to grant a permission.

To grant access for all user roles

DataPermission.FIND.GrantForAllRoles( dataObject );
DataPermission.UPDATE.GrantForAllRoles( dataObject );
DataPermission.REMOVE.GrantForAllRoles( dataObject );

where:

dataObject - an object for which you want to grant a permission.

To deny access for a user

DataPermission.FIND.DenyForUser( userId, dataObject );
DataPermission.UPDATE.DenyForUser( userId, dataObject );
DataPermission.REMOVE.DenyForUser( userId, dataObject );

where:

userId - ID of a user, for which you want to deny a permission.
dataObject - an object for which you want to deny a permission.

To deny access for a user role

DataPermission.FIND.DenyForRole( roleName, dataObject );
DataPermission.UPDATE.DenyForRole( roleName, dataObject );
DataPermission.REMOVE.DenyForRole( roleName, dataObject );

where:

roleName - name of  a user role, for which you want to deny a permission.
dataObject - an object for which you want to deny a permission.

To deny access for all users

DataPermission.FIND.DenyForAllUsers( dataObject );
DataPermission.UPDATE.DenyForAllUsers( dataObject );
DataPermission.REMOVE.DenyForAllUsers( dataObject );

where:

dataObject - an object for which you want to deny a permission.

To deny access for all user roles

DataPermission.FIND.DenyForAllRoles( dataObject );
DataPermission.UPDATE.DenyForAllRoles( dataObject );
DataPermission.REMOVE.DenyForAllRoles( dataObject );

where:

dataObject - an object for which you want to deny a permission.

Example:

The example below demonstrates how to deny retrieving an object for all roles. The code loads an object from the Address table and then uses the object reference to deny any FIND-related operation for all roles.

AsyncCallback<Object> denyCallback = new AsyncCallback<Object>(
  result =>
  {
    System.Console.WriteLine( "Permission has been denied for all roles" );
  },
  fault =>
  {
    System.Console.WriteLine( "Error - " + fault );
  } );

AsyncCallback<Address> searchCallback = new AsyncCallback<Address>(
  result =>
  {
    DataPermission.FIND.DenyForAllRoles( result, denyCallback );
  },
  fault =>
  {
    System.Console.WriteLine( "Error - " + fault );
  } )

Backendless.Data.Of<Address>().FindFirst( searchCallback );

 

Real-Time Database

Real-Time Database Overview

As users work with data in your application, the client program and the server-side business logic may create new, update or delete existing objects in the database. In some cases, it is important to instantly reflect these changes for other users of the app. For example, suppose a mobile and/or web app displays remaining available quantity for a product in an online store. As users purchase the product, it is important to update the remaining quantity for all other users of the app. The Backendless RT (Real-Time) Database system enables Backendless client applications to maintain constant real-time synchronization with the Backendless database.

The Real-Time Database functionality introduces a new library dependency. Make sure to read the Client-side Setup section for more information.

 

The Backendless RT system relies on listeners which can be registered at a table level. A listener is notified when an object is created, updated or deleted. For example, the following sample registers a listener which is notified when an object is created in the Person table:

IEventHandler<Dictionary<String, Object>> personTableRT;
personTableRT = Backendless.Data.Of( "Person" ).RT();

personTableRT.AddCreateListener( createdObject =>
{
    // a new object is created in the database
    // the object is represented by the "createdObject" variable,
    // which is of type Dictionary<String, Object>
});

Notice the callback method receives the created object. The callback is invoked any time a new object is saved in the database.

 

The RT system makes it possible to receive conditional updates. This means the listeners are notified only when objects which meet a certain criteria are created, updated or deleted. For example, the following code requests that any update in the Address table for the objects with the city name set to Tokyo  is delivered to the listener:

IEventHandler<Dictionary<String, Object>> addressTableRT;
adressTableRT = Backendless.Data.Of( "Address" ).RT();

personTableRT.AddUpdateListener( "cityName = 'Tokyo'", updatedObject =>
{
    // an object with the property name "cityName" is updated in the database.
    // The property value is "Tokyo". The object is represented by the
    // "updatedObject" variable, which is of type Dictionary<String, Object>
});

Notice the code uses a whereClause which creates a condition for the updated objects which must be delivered to the client:

cityName = 'Tokyo'

Any updated object in the Address table, which matches the specified condition, is instantly delivered to the client applications.

 

Backendless RT is a multi-platform database, it works across all supported languages and client SDKs. Objects can be created/updated/deleted in Backendless using any of the supported APIs will be triggering RT events regardless of the operating system running on the client-side. For example, an iOS client can create a Real-Time database listener who will be receiving events resulting from a REST client saving objects in the database.

Handlers, Events and Listeners

Backendless Real-Time Database emits events when objects are created, updated or deleted. These events, along with the object which triggered the event, can be delivered in real-time to your application code via listener objects.

 

In order to subscribe to an real-time event, your application code must obtain an event handler for a data table in the Backendless database. A handler channels real-time events to the added listeners. These events include information about objects which were created, updated or deleted in the table the handler represents. To obtain a handler for a table, use the following API:

IEventHandler<Dictionary<String, Object>> rtHandler = Backendless.Data.Of( "TABLE-NAME" ).RT();
IEventHandler<E> rtHandler = Backendless.Data.Of<E>().RT();

where:

TABLE-NAME - Name of the table to obtain a handler for.
YOUR-CLASS - an Objective-C/Swift class identifying the table to obtain a handler for.

 

Multiple invocations of the methods to get a handler for the same data table return a new instance of the handler. As a result, each handler maintains its own collection of listeners. This can provide additional convenience if different parts of the client application need to have its own instance of the handler. Alternatively, if the same handler must be shared across the entire application, it is simple to configure it as a singleton object.

Real-Time Event Listeners

In order to receive real-time events from the Backendless database, a listener must be registered using an event handler described above. A listener is an implementation of a delegate function which receives an object associated with the event. You can see specific implementations of the delegate in sections of the doc describing individual real-time events.

The diagram below illustrates the relationship between a data table in Backendless database, an API event handler and listeners handling real-time events:

handlers-listeners

The event handler object can be used for adding and removing listeners for the following events:

create - triggered when an object is saved in the database table represented by the event handler.
update - triggered when an object is updated in the database table represented by the event handler
delete - triggered when an object is deleted from the database table represented by the event handler
bulk update - triggered when multiple objects are updated in the database table represented by the event handler
bulk delete - triggered when multiple objects are deleted in the database table represented by the event handler

Object Created Event

Object Created Event Overview

Backendless Real-Time Database can notify your application code when new objects are created in the database. The create event is triggered when new objects are stored in the database by using the Backendless Data API or in Backendless Console. A listener for the create event can be notified about either any new object (unconditional delivery) or only those which match a pattern expressed as a where clause (conditional delivery).

 

An event listener is an object which receives a callback when an event is triggered by the real-time database. To add an event listener, it is necessary to obtain a handler for a database table first. The handler provides methods for registering event listeners.

 

Event listener functions for the Object Created Event must implement the following delegate:

public delegate void ObjectCreated<T>( T obj );

where T is a type representing an object in the Backendless database. If your code uses the Dictionary-based approach in the APIs, the type must be Dictionary<string, object>.

 

Conditional Delivery of New Objects

Consider the following data table. The table name is Order and it contains the orderAmount column which will be used in the example:

order-table-rt.zoom70

 

The code below adds a listener for the create event for the Order table.

var orderEventHandler = Backendless.Data.Of( "Order" ).RT();

orderEventHandler.AddCreateListener( "orderAmount > 1000", createdOrder =>
{
    Console.WriteLine( $"new Order object has been created. Object ID - {createdOrder["objectId"]}");
} );
Assume the following class represents objects from the Order table:
public class Order
{
  public string ObjectId{ get; set; }
  public string OrderName{ get; set; }
  public double OrderAmount{ get; set; }
}
Then the following code will create an event listener for any new orders where OrderAmount is greater than 1000:
var orderEventHandler = Backendless.Data.Of<Order>().RT();

orderEventHandler.AddCreateListener( "OrderAmount > 1000", createdOrder =>
{
    Console.WriteLine( $"new Order object has been created. Object ID - {createdOrder.ObjectId}");
} );

The event listener will receive only the Order objects where the amount of order is greater 1000. This is achieved with the first argument in the AddCreateListener method. The argument is a where clause condition, which references the orderAmount column and requests that the real-time database sends only the matching objects. The second argument is a callback object which will receive any new object which matches the where clause condition. Notice the argument of the delegate function method is the actual object saved in the database.

 

For more information about the whereClause syntax, see the Condition Syntax section of the guide.

Unconditional Delivery Listeners

The where clause argument in the addCreateListener method is optional. When it is not provided, the real-time database will deliver any new object created in the database's table:

var orderEventHandler = Backendless.Data.Of( "Order" ).RT();

orderEventHandler.AddCreateListener( createdOrder =>
{
  Console.WriteLine( $"new Order object has been created. Object ID - {createdOrder[ "objectId" ]}" );
} );
var orderEventHandler = Backendless.Data.Of<Order>().RT();

orderEventHandler.AddCreateListener( createdOrder =>
{
  Console.WriteLine( $"new Order object has been created. Object ID - {createdOrder.ObjectId}");
} );

Triggering Create Event

To trigger an event, create and save a new instance of the Order object in the Backendless database. This can be done with any Backendless SDK (Android, iOS, JS, .NET), REST API or Backendless Console:

var orderObject = new Dictionary<string, object>();
orderObject[ "OrderAmount" ] = 1200;
orderObject[ "OrderName" ] = "Sample Order";
Backendless.Data.Of( "Order" ).SaveAsync( orderObject );
Order class:
namespace TestApplication
{
 public class Order
 {
   public string ObjectId{ get; set; }
   public string OrderName{ get; set; }
   public double OrderAmount{ get; set; }
 }
}
Creating an instance of Order and saving it in the database:
var order = new Order
{
  OrderAmount = 1200,
  OrderName = "Sample Order"
};
Backendless.Data.Of<Order>().SaveAsync( order );

Removing Listeners

Listener objects can be removed when the application is no longer interested in receiving real-time updates. A listener (or all of them) should be removed from the same event handler object which they were added to. The following listener removal APIs are available:

 

Removing all listeners

The method removes all registered listeners for the create event:

eventHandler.RemoveCreateListeners();

The method below removes all listeners which process events for a specific where clause. The value of the whereClause argument must be the same as one in the AddCreateListener method:

eventHandler.RemoveCreateListeners( string whereClause );

Removing a specific listener

This method removes a listener identified by a delegate implementation:

eventHandler.RemoveCreateListener( ObjectCreated<T> objectCreated );

Object Updated Event

Object Updated Event Overview

When an object is updated in the Backendless Real-Time Database, it instantly notifies connected clients about the change using the update event. Objects can be updated either using the Backendless Data API or in Backendless Console. A listener for the update event can be notified about either updates of any object in the database or only those which match a pattern expressed as a where clause.

 

An event listener itself is an object which receives a callback when an event is triggered by the real-time database. To add an event listener, it is necessary to obtain a handler for a database table first. The handler provides methods for registering an event listener.

 

Backendless Database provides two APIs for object deletion: (1) single object deletion and (2) deletion in bulk. As a result, there are two different types of listeners which can be added to an event handler: Single Object Update Listener and Bulk Update Listener.

 

Event listener functions for the Object Updated Event must implement the following delegate:

public delegate void ObjectUpdated<T>( T obj );

where T is a type representing the updated object in the Backendless database. If your code uses the Dictionary-based approach in the APIs, the type must be Dictionary<string, object>.

 

Conditional Delivery of Updated Objects

Consider a data table called Order. The table contains a column named orderAmount. The code below adds a listener for the update event for the Order table.

var orderEventHandler = Backendless.Data.Of( "Order" ).RT();

orderEventHandler.AddUpdateListener( "orderAmount > 1000", updatedOrder =>
{
    Console.WriteLine( $"an Order object has been updated in the database. Object ID - {updatedOrder["objectId"]}");
} );
Assume the following class represents objects from the Order table:
public class Order
{
  public string ObjectId{ get; set; }
  public string OrderName{ get; set; }
  public double OrderAmount{ get; set; }
}
Then the following code will register an event listener for any existing orders where OrderAmount is greater than 1000:
var orderEventHandler = Backendless.Data.Of<Order>().RT();

orderEventHandler.AddUpdateListener( "OrderAmount > 1000", updatedOrder =>
{
    Console.WriteLine( $"an Order object has been updated in the database. Object ID - {updatedOrder.ObjectId}");
} );

The event listener will receive only the Order objects which are being updated in the database where the amount of order is greater 1000. This is achieved with the first argument in the AddUpdateListener method. The argument is a where clause condition, which references the orderAmount column and requests that the real-time database sends only the matching objects. The second argument is a callback object which will receive any updated object which matches the where clause condition. Notice the argument of the delegate function method is the actual object updated in the database.

 

For more information about the whereClause syntax, see the Condition Syntax section of the guide.

Unconditional Delivery Listeners

The where clause argument in the addUpdateListener method is optional. When it is not provided, the real-time database will deliver any object  which is being updated in the database's table:

var orderEventHandler = Backendless.Data.Of( "Order" ).RT();

orderEventHandler.AddUpdateListener( updatedOrder =>
{
  Console.WriteLine( $"an Order object has been updated in the database. Object ID - {updatedOrder[ "objectId" ]}" );
} );
var orderEventHandler = Backendless.Data.Of<Order>().RT();

orderEventHandler.AddUpdateListener( updatedOrder =>
{
  Console.WriteLine( $"an Order object has been updated in the database. Object ID - {updatedOrder.ObjectId}");
} );

Triggering Update Event

To trigger an event, update an existing Order object in the Backendless database. This can be done with any Backendless SDK (Android, iOS, JS, .NET), REST API or Backendless Console:

var orderObject = // retrieve an existing object from the database;

// update a property in the object
orderObject[ "OrderAmount" ] = 1200;

// now save the updated object
Backendless.Data.Of( "Order" ).SaveAsync( orderObject );
Order class:
namespace TestApplication
{
 public class Order
 {
   public string ObjectId{ get; set; }
   public string OrderName{ get; set; }
   public double OrderAmount{ get; set; }
 }
}
Updating an instance of Order and saving it in the database:
var order = // retrieve an order from the database

// update a property in the object
order.OrderAmount = 2000;

// save the object in the database
Backendless.Data.Of<Order>().SaveAsync( order );

Removing Listeners

Listener objects can be removed when the application is no longer interested in receiving real-time updates. A listener (or all of them) should be removed from the same event handler object which they were added to. The following listener removal APIs are available:

 

Removing all listeners

The method removes all registered listeners for the update event:

eventHandler.RemoveUpdateListeners();

The method below removes all listeners which process events for a specific where clause. The value of the whereClause argument must be the same as one in the AddUpdateListener method:

eventHandler.RemoveUpdateListeners( string whereClause );

Removing a specific listener

This method removes a listener identified by a delegate implementation:

eventHandler.RemoveUpdateListener( ObjectUpdated<T> objectUpdated );

Bulk Update Listener

Listeners for bulk update work very similarly to the ones for single oject update. The differences between single object and bulk update include the method names to add and remove listener objects as well as the object delivered to the listener.

 

Conditional bulk update listener

When an API client executes a bulkUpdate API request using the same whereClause value as the one specified in addBulkUpdateListener, the listener receives a real-time event.

var eventHandler = Backendless.Data.Of( "TABLE-NAME" ).RT();
eventHandler.AddBulkUpdateListener( whereClause, ( bulkEvent ) =>
{
        
});
where the second argument is an implementation of the following delegate:
public delegate void MultipleObjectsUpdated( BulkEvent bulkEvent );
The BulkEvent is declared as shown below:
namespace BackendlessAPI.RT.Data
{
  public class BulkEvent
  {
    // the where clause for which the bulk event has been triggered
    // the value can be used to retrieve the updated objects from the database
    public String WhereClause { get; }
    
    // number of updated objects
    public int Count  { get; }
  }
}
var eventHandler = Backendless.Data.Of<T>().RT();
eventHandler.AddBulkUpdateListener( whereClause, ( bulkEvent ) =>
{
        
});
where the second argument is an implementation of the following delegate:
public delegate void MultipleObjectsUpdated( BulkEvent bulkEvent );
The BulkEvent is declared as shown below:
namespace BackendlessAPI.RT.Data
{
  public class BulkEvent
  {
    // the where clause for which the bulk event has been triggered
    // the value can be used to retrieve the updated objects from the database
    public String WhereClause { get; }
    
    // number of updated objects
    public int Count  { get; }
  }
}

Unconditional bulk update listener

The registered listener will receive a real-time request for all bulkUpdate API requests.

var eventHandler = Backendless.Data.Of( "TABLE-NAME" ).RT();
eventHandler.AddBulkUpdateListener( ( bulkEvent ) =>
{
        
});
where the second argument is an implementation of the following delegate:
public delegate void MultipleObjectsUpdated( BulkEvent bulkEvent );
The BulkEvent is declared as shown below:
namespace BackendlessAPI.RT.Data
{
  public class BulkEvent
  {
    // this value will be null for the unconditional bulk update event listener
    public String WhereClause { get; set; }
    
    // number of updated objects
    public int Count  { get; set; }
  }
}
var eventHandler = Backendless.Data.Of<T>().RT();
eventHandler.AddBulkUpdateListener( ( bulkEvent ) =>
{
        
});
where the second argument is an implementation of the following delegate:
public delegate void MultipleObjectsUpdated( BulkEvent bulkEvent );
The BulkEvent is declared as shown below:
namespace BackendlessAPI.RT.Data
{
  public class BulkEvent
  {
    // this value will be null for the unconditional bulk update event listener
    public String WhereClause { get; set; }
    
    // number of updated objects
    public int Count  { get; set; }
  }
}

 

For both conditional and unconditional update listeners, the BulkEvent object received in the callback contains the following properties:

WhereClause - the where clause value used in the API call to identify the objects updated in the database.
Count - the number of objects updated in the database.

Removing listeners

The method removes all registered listeners for the bulk update event:

eventHandler.RemoveBulkUpdateListeners();

The method below removes all listeners which process events for a specific where clause. The value of the whereClause argument must be the same as one in the AddBulkUpdateListener method:

eventHandler.RemoveBulkUpdateListeners( string whereClause );

Removing a specific listener

This method removes a listener identified by the callback object:

eventHandler.RemoveBulkUpdateListener( MultipleObjectsUpdated callback );

Object Deleted Event

Object Deleted Event Overview

When an object is deleted from the Backendless Real-Time Database, it instantly notifies connected clients about the change using the delete event. Objects can be deleted either using the Backendless Data API or in Backendless Console. A listener for the delete event can be notified about the deletion of either any object in the database or only of those which match a pattern expressed as a where clause.

 

An event listener itself is an object which receives a callback when an event is triggered by the real-time database. To add an event listener, it is necessary to obtain a handler for a database table first. The handler provides methods for registering an event listener.

 

Backendless Database provides two APIs for object deletion: (1) single object deletion and (2) deletion in bulk. As a result, there are two different types of listeners which can be added to an event handler: Single Object Deletion Listener and Bulk Deletion Listener.

 

Event listener functions for the Object Deleted Event must implement the following delegate:

public delegate void ObjectDeleted<T>( T obj );

where T is a type representing the deleted object in the Backendless database. If your code uses the Dictionary-based approach in the APIs, the type must be Dictionary<string, object>.

 

Conditional Delivery Of Deleted Objects

Consider a data table called Order. The table contains a column named orderAmount. The code below adds a listener for the delete event for the Order table.

var orderEventHandler = Backendless.Data.Of( "Order" ).RT();

orderEventHandler.AddDeleteListener( "orderAmount > 1000", deletedOrder =>
{
    Console.WriteLine( $"an Order object has been deleted in the database. Object ID - {deletedOrder["objectId"]}");
} );
Assume the following class represents objects from the Order table:
public class Order
{
  public string ObjectId{ get; set; }
  public string OrderName{ get; set; }
  public double OrderAmount{ get; set; }
}
Then the following code will register an event listener for any orders to be deleted where OrderAmount is greater than 1000:
var orderEventHandler = Backendless.Data.Of<Order>().RT();

orderEventHandler.AddDeleteListener( "OrderAmount > 1000", deletedOrder =>
{
    Console.WriteLine( $"an Order object has been deleted in the database. Object ID - {deletedOrder.ObjectId}");
} );

The event listener will receive only the deleted Order objects with the order amount exceeding 100. This is achieved with the first argument in the AddDeleteListener method. The argument is a where clause condition, which references the orderAmount column and requests that the real-time database sends only the matching objects. The second argument is a callback object which will receive any deleted object which matches the where clause condition. Notice the argument of the delegate function method is the actual object deleted from the database.

 

For more information about the whereClause syntax, see the Condition Syntax section of the guide.

Unconditional Delivery Listeners

The where clause argument in the addDeleteListener method is optional. When it is not provided, the real-time database will deliver any object  which is being deleted in the database's table:

var orderEventHandler = Backendless.Data.Of( "Order" ).RT();

orderEventHandler.AddDeleteListener( deletedOrder =>
{
  Console.WriteLine( $"an Order object has been deleted from the database. Object ID - {deletedOrder[ "objectId" ]}" );
} );
var orderEventHandler = Backendless.Data.Of<Order>().RT();

orderEventHandler.AddDeleteListener( deletedOrder =>
{
  Console.WriteLine( $"an Order object has been deleted from the database. Object ID - {deletedOrder.ObjectId}");
} );

Triggering Delete Event

To trigger an event, delete an Order object from the Backendless database. This can be done with any Backendless SDK (Android, iOS, JS, .NET), REST API or Backendless Console:

var orderObject = // retrieve an existing object from the database;

// now delete the object
Backendless.Data.Of( "Order" ).RemoveAsync( orderObject );
Order class:
namespace TestApplication
{
 public class Order
 {
   public string ObjectId{ get; set; }
   public string OrderName{ get; set; }
   public double OrderAmount{ get; set; }
 }
}
Updating an instance of Order and saving it in the database:
var order = // retrieve an order from the database

// delete the object in the database
Backendless.Data.Of<Order>().RemoveAsync( order );

Removing Listeners

Listener objects can be removed when the application is no longer interested in receiving real-time updates. A listener (or all of them) should be removed from the same event handler object which they were added to. The following listener removal APIs are available:

 

Removing all listeners

The method removes all registered listeners for the delete event:

eventHandler.RemoveDeleteListeners();

The method below removes all listeners which process events for a specific where clause. The value of the whereClause argument must be the same as one in the AddDeleteListener method:

eventHandler.RemoveDeleteListeners( string whereClause );

Removing a specific listener

This method removes a listener identified by a delegate implementation.

eventHandler.RemoveDeleteListener( ObjectDeleted<T> objectDeleted );

Bulk Delete Listener

Listeners for bulk deletion work identically to the ones for single deletion. The difference is in the method names to add and remove listener objects:

 

Conditional bulk delete listener

A conditional bulk delete listener receives an event when an API client uses the BulkDelete API to delete multiple objects in a table with the same whereClause statement as specified by the RT client:

var eventHandler = Backendless.Data.Of( "TABLE-NAME" ).RT();
eventHandler.AddBulkDeleteListener( whereClause, ( bulkEvent ) =>
{
        
});
where the second argument is an implementation of the following delegate:
public delegate void MultipleObjectsDeleted( BulkEvent bulkEvent );
The BulkEvent is declared as shown below:
namespace BackendlessAPI.RT.Data
{
  public class BulkEvent
  {
    // the where clause for which the bulk event has been triggered
    public String WhereClause { get; }
    
    // number of deleted objects
    public int Count  { get; }
  }
}
var eventHandler = Backendless.Data.Of<T>().RT();
eventHandler.AddBulkDeleteListener( whereClause, ( bulkEvent ) =>
{
        
});
where the second argument is an implementation of the following delegate:
public delegate void MultipleObjectsDeleted( BulkEvent bulkEvent );
The BulkEvent is declared as shown below:
namespace BackendlessAPI.RT.Data
{
  public class BulkEvent
  {
    // the where clause for which the bulk event has been triggered
    public String WhereClause { get; }
    
    // number of deleted objects
    public int Count  { get; }
  }
}

Unconditional bulk delete listener

A unconditional bulk delete listener receives events when a client uses the bulkDelete API with any whereClause statement:

var eventHandler = Backendless.Data.Of( "TABLE-NAME" ).RT();
eventHandler.AddBulkDeleteListener( ( bulkEvent ) =>
{
        
});
where the second argument is an implementation of the following delegate:
public delegate void MultipleObjectsDeleted( BulkEvent bulkEvent );
The BulkEvent is declared as shown below:
namespace BackendlessAPI.RT.Data
{
  public class BulkEvent
  {
    // this value will be null for the unconditional bulk delete event listener
    public String WhereClause { get; set; }
    
    // number of deleted objects
    public int Count  { get; set; }
  }
}
var eventHandler = Backendless.Data.Of<T>().RT();
eventHandler.AddBulkDeleteListener( ( bulkEvent ) =>
{
        
});
where the second argument is an implementation of the following delegate:
public delegate void MultipleObjectsDeleted( BulkEvent bulkEvent );
The BulkEvent is declared as shown below:
namespace BackendlessAPI.RT.Data
{
  public class BulkEvent
  {
    // this value will be null for the unconditional bulk update event listener
    public String WhereClause { get; set; }
    
    // number of deleted objects
    public int Count  { get; set; }
  }
}

 

Both conditional and unconditional event listeners receive the following information in the callback object. The properties listed below are included  into the BulkEvent object delivered to the listener's callback.

WhereClause - the where clause value used in the API call to identify the objects deleted in the database.
Count - the number of objects deleted in the database.

Removing listeners

The method removes all registered listeners for the BulkDelete event:

eventHandler.RemoveBulkDeleteListeners();

The method below removes all listeners which process events for a specific where clause. The value of the whereClause argument must be the same as one in the AddBulkDeleteListener method:

eventHandler.RemoveBulkDeleteListeners( string whereClause );

Removing a specific listener

This method removes a listener identified by the callback object:

eventHandler.RemoveBulkUpdateListener( MultipleObjectsDeleted callback );

Condition Syntax

Real-Time API to register listeners for the create, update and delete events use a special argument called whereClause. The argument is a string value which sets a condition which the created, updated or deleted objects must match for the real-time event to be delivered to the listeners. The Backendless Real-Time Database supports a subset of the general where clause syntax:

Condition

Sample where clause

Value in a column equals a string literal.

columnName = 'foo'

Value in a column does not equal a string literal.

columnName != 'foo'

Value in a column is not assigned

columnName is null

Value in a column is assigned

not(columnName is null)

Value in a column of the Boolean type equals true

boolColumnName = true

Value in a column of the Boolean type equals false

boolColumnName = false

Value in a column of the Boolean type is null

boolColumnName = null

Value in a column contains a substring literal

columnName LIKE '%foo%'

Value in a column ends with a substring literal

columnName LIKE '%foo'

Value in a column starts with a substring literal

columnName LIKE 'foo%'

Value in a column is one of enumerated string literals.

columnName IN ('value1', 'value2', 'value3)
 
or
 
columnName = 'value1' OR columnName = 'value2' or columnName = 'value3'

Value in a column equals a numeric literal

intColumnName = 21
 
doubleColumnName = 123.456

Value in a column does not equal a numeric literal

intColumnName != 21
 
doubleColumnName != 123.456

Value in a column is greater than a numeric literal

intColumnName > 21

Value in a column is greater than or equal a numeric literal

intColumnName >= 21

Value in a column is less than a numeric literal

intColumnName < 21

Value in a column is less than or equal a numeric literal

intColumnName <= 21

 

Multiple expressions referencing same or different columns in the same table can be grouped using the AND and OR operators:

Condition

Sample where clause

Value in the columnX equals a string literal OR value in the columnY equals a number literal.

columnNameX = 'foo' OR columnNameY = 21

Value in the columnX equals a string literal AND value in the columnY equals a string literal.

columnNameX = 'foo' AND columnNameY = 'bar'

 

The evaluation priority of multiple expressions grouped with the AND and OR operators can be controlled with parenthesis. For example:

 

column1 = 'bar' OR (column1 = 'foo' AND column2 = 123) OR (column1 = 'abc' AND column3 = 777)

 

The syntax in the real-time system excludes the following, more complex scenarios:

References to related tables and columns in the related tables
statements with SubQuery

 

It is however possible to architect a solution to enable more complex whereClause conditions using the following approach:

1. For a complex whereClause scenario identify a smaller condition would be supported per the table above.
2. Add a conditional listener using the smaller whereClause.
3. When an object is delivered for the create or update events, run an API request with the following whereClause:
complex-whereClause-from-step-1 AND receivedObject.objectId = 'objectId value of the received object'
4. If the query above returns the same object, it satisfies the more complex query.

Security and Permissions

Real-Time Database events account for permissions assigned to users and roles. For instance, if a user/role does not have the Find permission for a data table and the app with the logged in user registers the Object Created event listener, the app will not receive any of the created objects due to the security restriction.

Security enforcement for the Real-Time Database is available only in the Cloud9, Cloud99 plans in Backendless Cloud and in Managed Backedless and Backendless Pro versions of the product.

The security model (set of roles and permissions) for the Real-Time Database is the same as for the rest of the application. In other words, security configuration is done once and applies to both traditional APIs as well as the Real-Time Database. For more information on Backendless Security see the following sections:

User Roles

Global Permissions

Data Table Permissions

Individual Object Permissions

The most important topic of all is: Data Security

Connection Management

A client application running on a device or in a browser maintains a connection with the Backendless Real-Time Database. There is always only one connection for a single client, even though it may have multiple listeners for different data tables. The connection status can be tracked in the application using connection event handlers:

 

Connection Established Event

The event is dispatched when the client application establishes a connection with the Real-Time Database. Once a connection is established, real time database dispatches events triggered by the API calls to the client application. Add the following listener to be notified when the connection is established:

Backendless.RT.AddConnectListener( () =>
{
  Console.WriteLine( "Client connected" );
});

The "Connection Established" event takes place when the client client application adds its very first API event listener. Additionally, the event may be triggered when the client reconnects with the real-time database after the connection is interrupted.

 

Disconnect Event

If the connection with the real-time database is dropped either due to network or server problems, the client application is notified via the following listener:

Backendless.RT.AddDisconnectListener( ( reason ) =>
{
  Console.WriteLine( $"Client disconnected. Disconnect reason - {reason}" );
});

Cloud Code - Business Logic

Real-Time Database emits events which can be processed in Cloud Code to add additional logic and functionality in your application. Supported events include:

Event

Event name

Event timing

Event arguments

Blocking/Non-blocking

Before client connected

RT_beforeClientConnected

The event is emitted at the time when a Real-Time connection is established. The exact timing of the event is right before Backendless notifies the client application that the connection is established. This means the Cloud Code event handler can cancel the connection.

connectionId - a unique ID assigned by Backendless to the connection.
 
clientId - a unique ID assigned by Backendless to the client. The same client may open multiple connections. All of them will share the same clientId.

Blocking

After client connected

RT_afterClientConnected

The event is emitted after Backendless handles the connection processing and the client application is informed that the connection has been established.

connectionId, clientId (see the description above)

Non-blocking

Client disconnected

RT_clientDisconnected

Occurs when the client disconnects from the Real-Time database.

connectionId, clientId (see the description above)

Non-blocking

User logs in or logs out

RT_clientUserPresenceChanged

Occurs when a user logs in to or logs out from Backendless using the Backendless Login API.

connectionId, clientId (see the description above)

prevUserId - when the value is null, this is the login event, otherwise, it is logout.

Non-blocking

 

Creating a Cloud Code Event Handler

Handling of the Cloud Code events must be done in an event handler. An event handler can be created with Java, JavaScript or using the Codeless programming. The simplest way to create a Cloud Code event handler is by using Backendless Console:

1. Login to Backendless Console and select your app.
2. Click the Business Logic icon in the icon panel on the left.
3. Click the EVENT HANDLERS tab.
4. Click the New Event Handler link.
5. Select the language of your choice in the Language drop-down list.
6. Select or create a model in the Model drop-down list. To learn more about models, see the Deployment Models section of the Cloud Code development guide.
7. Select Real Time Events in the Category drop-down list.
8. The Event drop-down list contains a list of Real-Time events. You will see the events described in the table above. Other events in the list pertain to the Real-Time Messaging Cloud Code events. Select an event from the list. For the clientConnected event, select the before or after timing.
9. Click SAVE to create the event code placeholder.

 

The generated event handler will appear similar to the one shown below:

rt-event-handler-js

 

You can make code changes directly in the browser. When ready to deploy, click the blue Save and Deploy All button located below the coding block. Alternatively, the code can be downloaded directly from Backendless Console so it can be edited, debugged and deployed to Backendless from the developer's machine. To download generated code, click the Download menu and select JS:

download-js-code

Once the code is downloaded, make sure to run npm i to install all the dependencies. Fore more information on local debugging and deployment of the code from the developer's machine, see the following sections in the Cloud Code for JS developer guide:

Debugging

Deployment

Real-Time Messaging

Real-Time Messaging Overview

Messaging is an essential function of mobile and desktop applications. It can be used for a multitude of functions including chat, private messaging, system update broadcast, maintaining game scores, etc. The Backendless Messaging Service provides the API enabling the publish-subscribe message exchange pattern. With the pattern, one part of the code can subscribe to receive messages and another publishes messages. A message can be any data - Backendless supports messages of primitive or complex data types.

Backendless publish/subscribe system relies on the following concepts:

channel - a logical medium "transporting" the messages.
publisher - a program (or a part of it) which uses the Publishing API to send messages to a channel.
subscriber - a program (or a part of it) which uses the Subscription API to receive messages from a channel.

 

Subscribers connect to a channel (or multiple channels) and messages are published into a channel. By default Backendless sends all messages published into a channel to all the subscribers of the channel. However, a subscription can include message filters, in this case, Backendless delivers only the messages matching the filter.

About Messages

Messages may be published in different data types and formats. For example, a message can be a primitive value (i.e. a number or a string) or a complex object structure, it could be a JSON object published by a JS or a REST client or a Java object published by an Android or Java app. Adding to the mix iOS publishers and business logic (Cloud Code) which can also publish messages creates a wide variety of possibilities for what a message may look like. The subscriber API offers the flexibility to receive messages which are cast to a specific data type or messages represented in the internal Backendless format. The latter provides access to additional metadata, such as publisher ID and headers.

 

It is important for both publisher and subscriber to "agree" on the message format. For example, if published messages are strings, the registered listener should be expecting strings. If the published messages are objects of a particular type, the listener should be coded to receive objects of that type.

Subscription API

Introduction

In order to receive published messages, application must subscribe to a channel. The subscription API returns a Channel object which can  be used to add messaging callbacks. A callback is used by Backendless to deliver published messages to the client application.

 

Channel Subscription

This API subscribes to a channel. Once the channel object is obtained, it can be used to add message listeners/callbacks.

public Channel Backendless.Messaging.Subscribe( string channelName )

where:

channelName - name of the channel to subscribe to.

Return value:

Channel object, can is used to add messaging listeners/callbacks.

Receiving Unfiltered Messages

Backendless Real-Time Messaging delivers messages to listeners added to a channel. A listener is a callback function which is invoked by Backendless to deliver a message to the client application. Depending on the API used to add a listener, it can receive either all messages published into the channel or a filtered subset of messages. The filtering options are explained in greater detail in the Receiving Filtered Messages section of this guide.

 

Receiving String Messages

Adding a listener to receive messages as string objects. Messages should be published as string objects):

Receiving Dictionary/Map messages

Adding a listener to receive messages as dictionary/map objects. Messages can be published as JSON objects, complex types or disctionary/map objects:

Receiving custom type/class messages

Adding a listener to receive messages as instances of a custom type (the Person class). Published messages must be instances of the class, map/dictionary objects or JSON objects with the same structure as the class:

General signature

A general signature for receiving messages as instances of a custom type:

Receiving messages in Backendless internal format

Adding a listener to receive message in the Backendless internal format:

 

Receiving Filtered Messages

Backendless message filtering is a powerful mechanism enabling conditional message delivery, interest-based subscriptions and private (point-to-point) messaging. To enable filtering, a messaging listener registration must include a special condition called selector. Backendless applies the selector to every message published into a channel and if the condition is true, the message is delivered to the subscriber.

 

A selector is a query expressed in the SQL-92 syntax and formatted as the condition part of the SQL's WHERE clause. The condition references headers in the published messages. When a message is published and a subscriber has a selector, Backendless checks the condition of the selector against the headers of the published message. If the result of the condition is true, the message is delivered to the subscriber.

 

General signature

A general signature for receiving filtered messages as instances of a custom type:

Filtering of String Messages

Adding a listener to receive filtered messages as string objects:

Filtering of Dictionary/Map messages

Adding a listener to receive filtered messages as dictionary/map objects:

Filtering of custom type/class messages

Adding a listener to receive messages as instances of a custom type (the Person class):

Filtering of messages in Backendless internal format

Adding a listener to receive message in the Backendless internal format:

 

Removing Listeners

To stop receiving messages listeners can be removed using the following API:

 

 

Removing all message listeners for a selector:

 

Remove all listeners for a channel:

Example

The following example declares a messaging listener, adds and then removes it. The example as shown does not provide any subscription functionality as the listener is removed right after it is added, however, it clearly demonstrates how a listener can be removed from a channel:

 

 

Channel State

A subscribed channel can be one of the following two states: joined or left. A joined channel is active and all messaging listeners/callbacks received published messages. When a channel is in the left state, the subscription is put on hold and no messages arrive to the added listeners/callbacks. When a channel subscription is established, the initial state is joined. The following API provides access to control channel's state:

Leaving Channel

Joining Channel

Checking Status

 

Publishing API

General API

A Backendless application can publish messages to Backendless for subsequent distribution to subscribers. A message must be published to a channel. Backendless supports unlimited number of channels per application. Channels can be used as a filtering mechanism - subscribers see the messages published only to the channel they subscribe to.

 

There is only one API specification for message publishing. It supports the following scenarios:

Publishing with headers - can be used for conditional delivery

 

API Specification

Blocking Methods

Publishes a message to the default channel.

public MessageStatus Backendless.Messaging.Publish( object message )

Same as above, but publishes into the specified channel.

public MessageStatus Backendless.Messaging.Publish( object message,
                                                    string channelName )

Publishes a message to default channel. Message may have headers and/or subtopic defined in the publishOptions argument.

public MessageStatus Backendless.Messaging.Publish( object message, 
                                                    PublishOptions publishOptions )

Same as above, but publishes into the specified channel.

public MessageStatus Backendless.Messaging.Publish( object message, 
                                                    string channelName,
                                                    PublishOptions publishOptions )

 

Publishes a message to the default channel. Message may have headers and/or subtopic defined in the publishOptions argument. The deliveryOptions argument can be used to:

1. Publish the message at a later time (delayed publishing).
2. Specify that the message should be republished multiple times (repeated publishing).

public MessageStatus Backendless.Messaging.Publish( object message, 
                                                    PublishOptions publishOptions, 
                                                    DeliveryOptions deliveryOptions )

Same as above, but publishes into the specified channel.

public MessageStatus Backendless.Messaging.Publish( object message,
                                                    string channelName,
                                                    PublishOptions publishOptions,
                                                    DeliveryOptions deliveryOptions )

Non-blocking Methods:

The same set of methods is available for the asynchronous invocation. The difference in the method signatures is the AsyncCallback<MessageStatus> argument:

// Publishes message to "Default" channel.
public MessageStatus Backendless.Messaging.Publish( object message,
                                                    AsyncCallback<MessageStatus> publishResponder )

// Same as above, but published into the specified channel.
public MessageStatus Backendless.Messaging.Publish( object message,
                                                    string channelName,
                                                    AsyncCallback<MessageStatus> publishResponder )

// Publishes message to "Default" channel. Message may have headers and/or subtopic defined in the 
// publishOptions argument.
public MessageStatus Backendless.Messaging.Publish( object message, 
                                                    PublishOptions publishOptions,
                                                    AsyncCallback<MessageStatus> publishResponder )

// Same as above, but published into the specified channel.
public MessageStatus Backendless.Messaging.Publish( object message,
                                                    string channelName, 
                                                    PublishOptions publishOptions,
                                                    AsyncCallback<MessageStatus> publishResponder )

// Publishes message to "Default" channel. Message may have headers and/or subtopic 
// defined in the publishOptions argument. The deliveryOptions argument can be used to: 
// (1) designate the message as "push notification only" or both a push notification 
// and a pub/sub message; (2) publish the message at a later time (delayed publishing); 
// (3) specify that the message should be republished multiple times (repeated publishing).
public MessageStatus Backendless.Messaging.Publish( object message, 
                                                    PublishOptions publishOptions, 
                                                    DeliveryOptions deliveryOptions,
                                                    AsyncCallback<MessageStatus> publishResponder )

// Same as above, but published into the specified channel.
public MessageStatus Backendless.Messaging.Publish( object message,
                                                    string channelName,
                                                    PublishOptions publishOptions,
                                                    DeliveryOptions deliveryOptions,
                                                    AsyncCallback<MessageStatus> publishResponder )

where:

channelName - name of the channel to publish the message to. If the channel does not exist, Backendless automatically creates it.
message - object to publish. The object can be of any data type - a primitive value, String, Date, a  user-defined complex type, a collection or an array of these types.
publishOptions - an instance of PublishOptions. When provided may contain publisher ID (an arbitrary, application-specific string value identifying the publisher), subtopic value and/or a collection of headers. See the Delayed Publish and Repeated Publish sections for examples.
deliveryOptions - an instance of DeliveryOptions. When provides may specify options for message delivery such as: deliver as a push notification, delayed delivery or repeated delivery. See the Delayed Publish and Repeated Publish sections for examples.

Return value:

MessageStatus - a data structure which contains ID of the published message and the status of the publish operation. Available properties in the returned object include:

Status - status of the published message as defined in the BackendlessAPI.Messaging.PublishStatusEnum enumeration.

MessageId - an ID assigned to the message by the backend (the ID is needed to cancel the message).

ErrorMessage - When the message Status is returned as PublishStatusEnum.FAILED, the corresponding error message is available through the ErrorMessage property.

Errors:

The following errors may occur during the message publishing API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

 

Error Code

Description

5003

Invalid repeatExpiresAt date in delivery options.

5007

User does not have the permission to publish messages

5030

Invalid publishAt date in the delivery options.

Basic Publish

The following example publishes a message to the default channel:

Blocking API

using BackendlessAPI;
using BackendlessAPI.Messaging;
Backendless.InitApp( appId, secretKey, version ); // where to get the values for the InitApp call

MessageStatus status = Backendless.Messaging.Publish( "hello world" );
System.Console.WriteLine( "Message published. Message ID - " + status.MessageId + ". Message Status - " + status.Status );

Non-blocking Publish

using BackendlessAPI;
using BackendlessAPI.Messaging;
Backendless.InitApp( appId, secretKey, version ); // where to get the values for the InitApp call

AsyncCallback<MessageStatus> callback = new AsyncCallback<MessageStatus>(
        result =>
        {
          System.Console.WriteLine( "Message published. Message ID - " + result.MessageId + ". Message Status - " + result.Status );
        },

        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

Backendless.Messaging.Publish( "Hello world", callback );

Publish with Headers

Message headers is a collection of name/value pairs. A subscriber can set a filter expressed as an SQL "where clause" query (called selector) which Backendless uses to determine whether a message should be delivered to the subscriber. When the query matches the published data in message headers, message is delivered to the corresponding subscriber. For more information see the Conditional Delivery section of this guide.

 

The following example publishes a message with the city : Tokyo header:

Blocking Publish

using BackendlessAPI;
using BackendlessAPI.Messaging;
Backendless.InitApp( appId, secretKey, version ); // where to get the values for the InitApp call

PublishOptions publishOptions = new PublishOptions();
publishOptions.AddHeader( "city", "Tokyo" );

Weather weather = new Weather();
weather.Humidity = 80;
weather.Temperature = 78;

MessageStatus status = Backendless.Messaging.Publish( weather, publishOptions );
System.Console.WriteLine( "Message published. Message ID - " + status.MessageId + ". Message Status - " + status.Status );

Non-blocking Publish

using BackendlessAPI;
using BackendlessAPI.Messaging;
Backendless.InitApp( appId, secretKey, version ); // where to get the values for the InitApp call

AsyncCallback<MessageStatus> callback = new AsyncCallback<MessageStatus>(
        result =>
        {
          System.Console.WriteLine( "Message published. Message ID - " + result.MessageId + ". Message Status - " + result.Status );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

PublishOptions publishOptions = new PublishOptions();
publishOptions.AddHeader( "city", "Tokyo" );

Weather weather = new Weather();
weather.Humidity = 80;
weather.Temperature = 78;
Backendless.Messaging.Publish( weather, publishOptions, callback );

 

Delayed Delivery

Publishers can specify the time when the message should be processed and delivered to subscribers. This can be done in addition to all other publishing options (basic publish, with headers. Scheduled messages can be canceled at any time using the message cancellation API.

Blocking Publish

using BackendlessAPI;
using BackendlessAPI.Messaging;
Backendless.InitApp( appId, secretKey, version ); // where to get the values for the InitApp call

DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.PublishAt = DateTime.Now.AddSeconds( 20 );

MessageStatus status = Backendless.Messaging.Publish( "This message was scheduled 20 seconds ago", deliveryOptions );
System.Console.WriteLine( "Message scheduled. Message ID - " + status.MessageId + ". Message Status - " + status.Status );

if( status.Status == PublishStatusEnum.FAILED )
  System.Console.WriteLine( "Message publish failed with error " + status.ErrorMessage );

Non-blocking Publish

using BackendlessAPI;
using BackendlessAPI.Messaging;
Backendless.InitApp( appId, secretKey, version ); // where to get the values for the InitApp call

AsyncCallback<MessageStatus> callback = new AsyncCallback<MessageStatus>(
        result =>
        {
          System.Console.WriteLine( "Message scheduled. Message ID - " + result.MessageId + ". Message Status - " + result.Status );

          if( result.Status == PublishStatusEnum.FAILED )
            System.Console.WriteLine( "Message publish failed with error " + result.ErrorMessage );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.PublishAt = DateTime.Now.AddSeconds( 20 );

Backendless.Messaging.Publish( "This message was scheduled 20 seconds ago", deliveryOptions, callback );

 

Repeated Publish

Backendless supports repeated message processing - a message is published once, but is delivered to subscribers with the specified frequency. Repeated delivery will stop either at the specified time unless it is canceled using the message cancellation API.

Synchronous Publish

using BackendlessAPI;
using BackendlessAPI.Messaging;
Backendless.InitApp( appId, secretKey, version ); // where to get the values for the InitApp call

DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.PublishAt = DateTime.Now.AddSeconds( 20 );
deliveryOptions.RepeatEvery = 20;
deliveryOptions.RepeatExpiresAt = DateTime.Now.AddHours( 1 );

MessageStatus status = Backendless.Messaging.Publish( "This message was scheduled 20 seconds ago", deliveryOptions );
System.Console.WriteLine( "Message scheduled. Message ID - " + status.MessageId + ". Message Status - " + status.Status );

if( status.Status == PublishStatusEnum.FAILED )
  System.Console.WriteLine( "Message publish failed with error " + status.ErrorMessage );

Asynchronous Publish

using BackendlessAPI;
using BackendlessAPI.Messaging;
Backendless.InitApp( appId, secretKey, version ); // where to get the values for the InitApp call

AsyncCallback<MessageStatus> callback = new AsyncCallback<MessageStatus>(
        result =>
        {
          System.Console.WriteLine( "Message scheduled. Message ID - " + result.MessageId + ". Message Status - " + result.Status );

          if( result.Status == PublishStatusEnum.FAILED )
            System.Console.WriteLine( "Message publish failed with error " + result.ErrorMessage );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.PublishAt = DateTime.Now.AddSeconds( 20 );
deliveryOptions.RepeatEvery = 20;
deliveryOptions.RepeatExpiresAt = DateTime.Now.AddHours( 1 );

Backendless.Messaging.Publish( "This message was scheduled 20 seconds ago", deliveryOptions, callback );

Get Message Status

Backendless server handles message publishing as an autonomous task. As a result, the final status of the message publishing operation is not available right away. To retrieve the status, the client application must make a separate API call as documented below:

 

Cancel Scheduled Message

Delayed delivery as well as repeated publish messages can be canceled using the API documented below:

 

Synchronous Method:

public bool Backendless.Messaging.Cancel( string messageId )

Asynchronous Method:

public MessageStatus Backendless.Messaging.Publish( string messageId, AsyncCallback<bool> cancelResponder )

where:

messageId - ID of the message to cancel. Message ID must be obtained from the MessageStatus object obtained as the result of the Publish methods.

Return value:

bool -True is the scheduled message has been successfully canceled, false otherwise.

Errors:

The following errors may occur during the message cancellation API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

Error Code

Description

5040

Message has already been canceled or does not exist.

Example:

Synchronous Publish and Cancellation

using BackendlessAPI;
using BackendlessAPI.Messaging;
Backendless.InitApp( appId, secretKey, version ); // where to get the values for the InitApp call

DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.PublishAt = DateTime.Now.AddSeconds( 200 );

MessageStatus status = Backendless.Messaging.Publish( "This message was scheduled 20 seconds ago", deliveryOptions );
System.Console.WriteLine( "Message scheduled. Message ID - " + status.MessageId + ". Message Status - " + status.Status );

Backendless.Messaging.Cancel( status.MessageId );

Asynchronous Publish and Cancellation

using BackendlessAPI;
using BackendlessAPI.Messaging;
Backendless.InitApp( appId, secretKey, version ); // where to get the values for the InitApp call

AsyncCallback<MessageStatus> cancelCallback = new AsyncCallback<MessageStatus>(
        result =>
        {
          System.Console.WriteLine( "Message canceled - " + result );

        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

AsyncCallback<MessageStatus> publishCallback = new AsyncCallback<MessageStatus>(
        result =>
        {
          System.Console.WriteLine( "Message scheduled. Message ID - " + result.MessageId + ". Message Status - " + result.Status );
          Backendless.Messaging.Cancel( result.MessageId, cancelCallback );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.PublishAt = DateTime.Now.AddSeconds( 200 );

Backendless.Messaging.Publish( "This message was scheduled 20 seconds ago", deliveryOptions, publishCallback );

 

Pub/Sub Examples

Publish Strings

The example demonstrates publishing a string value and creating a subscriber for string messages. The message is published into the demo channel.

Subscriber

Publisher

curl -X POST \
  https://api.backendless.com/APP-ID/REST-API-KEY/messaging/demo \
  -H 'content-type: application/json' \
  -d '{  "message": "It's a great day!" }'
Backendless.Messaging.publish( "demo", "It's a great day!" )
 .then( function( result ) {
    console.log( "message has been published" );
  })
 .catch( function( error ) {
    console.log( "error publishing the message - " + error );
  })
Backendless.Messaging.publish( "demo", 
                               "It is a great day!", 
                               new AsyncCallback<MessageStatus>()
{
  @Override
  public void handleResponse( MessageStatus response )
  {
    Log.i( "MYAPP", "Message has been published" );  
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "Error publishing the message" );
  }
} );
backendless.messaging.publish("demo", message: "It is a great day!", response: { response in
    print("Message has been published")
}, error: { fault in
    print("Error: \(fault?.message ?? "")")
})
[backendless.messaging publish:@"demo" message:@"It is a great day!" response:^(MessageStatus *response) {
    NSLog(@"Message has been published");
} error:^(Fault *fault) {
    NSLog(@"Error: %@", fault.message);
}];

Publish Custom Type

The example demonstrates publishing a value of a custom type (Person) and creating a subscriber to receive objects of that type. The message is published into the demo channel.

Subscriber

Publisher

curl -X POST \
  https://api.backendless.com/APP-ID/REST-API-KEY/messaging/demo \
  -H 'content-type: application/json' \
  -d '{  "message": { "name":"Joe", "age":30}}'
function Person() {

}
 
var personObj = new Person();
personObj.name = "Joe";
personObj.age = 30;

Backendless.Messaging.publish( "demo", personObj )
 .then( function( result ) {
    console.log( "message has been published" );
  })
 .catch( function( error ) {
    console.log( "error publishing the message - " + error );
  })
public class Person
{
  // can also use getter/setter methods 
  // instead of public fields
  public String name;
  public int age;
}

Person personObject = new Person();
personObject.name = "Joe";
personObject.age = 30;

Backendless.Messaging.publish( "demo", 
                               personObject, 
                               new AsyncCallback<MessageStatus>()
{
  @Override
  public void handleResponse( MessageStatus response )
  {
    Log.i( "MYAPP", "Message has been published" );  
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "Error publishing the message" );
  }
} );
backendless.messaging.publish("demo", message: personObject, response: { response in
    print("Message has been published")
}, error: { fault in
    print("Error: \(fault?.message ?? "")")
})      
Person *personObject = [Person new];
personObject.name = @"Joe";
personObject.age = 30;
    
[backendless.messaging publish:@"demo" message:personObject response:^(MessageStatus *response) {
    NSLog(@"Message has been published");
} error:^(Fault *fault) {
    NSLog(@"Error: %@", fault.message);
}];

Publish Map/Dictionary

The example demonstrates publishing a value of an untyped data structure and creating a subscriber to receive objects which contain properties of the published object. The message is published into the demo channel.

Subscriber

Publisher

curl -X POST \
  https://api.backendless.com/APP-ID/REST-API-KEY/messaging/demo \
  -H 'content-type: application/json' \
  -d '{  "message": { "name":"Joe", "age":30}}'
var personObj = { name:"Joe", age:30 };

Backendless.Messaging.publish( "demo", personObj )
 .then( function( result ) {
    console.log( "message has been published" );
  })
 .catch( function( error ) {
    console.log( "error publishing the message - " + error );
  })
HashMap personObject = new HashMap();
personObject.put( "name", "Joe" );
personObject.put( "age", 30 );

Backendless.Messaging.publish( "demo", 
                               personObject, 
                               new AsyncCallback<MessageStatus>()
{
  @Override
  public void handleResponse( MessageStatus response )
  {
    Log.i( "MYAPP", "Message has been published" );  
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "Error publishing the message" );
  }
} );
let personObject: [String : Any] = ["name" : "Joe", "age" : 30];
backendless.messaging.publish("demo", message: personObject, response: { response in
    print("Message has been published")
}, error: { fault in
    print("Error: \(fault?.message ?? "")")
})
NSDictionary *personObject = @{@"name" : @"Joe", @"age" : @30};
[backendless.messaging publish:@"demo" message:personObject response:^(MessageStatus *response) {
    NSLog(@"Message has been published");
} error:^(Fault *fault) {
    NSLog(@"Error: %@", fault.message);
}];

Publish in the Future

The example demonstrates scheduling a message to be delivered on January 1st, 2030 at 00:00 Central time. The message is published into the demo channel:

 

Conditional Pub/Sub

The example demonstrates publishing a weather forecast message with headers which can be used to create a conditional subscriber. The subscriber uses a selector to get weather forecast messages for the Dallas area when the expected temperature is higher than 60F degrees. The message is published into the demo channel.

Subscriber

Publisher

curl -X POST \
  https://api.backendless.com/APP-ID/REST-API-KEY/messaging/demo \
  -H 'content-type: application/json' \
  -d '{  "message": "Thunderstorms likely. High 62F. Winds NNW at 10 to 15 mph. Chance of rain 90%.", 
         "headers" : { 
             "city" : "Dallas",
             "state" : "Texas", 
             "temperature":62    
         }                  
      }'
var message = "Thunderstorms likely. High 62F. Winds NNW at 10 to 15 mph. Chance of rain 90%.";
var publishOptions = new Backendless.PublishOptions({
       headers: {
          "city": "Dallas",
          "state": "Texas",
          "temperature": 62
       }
   });

Backendless.Messaging.publish( "demo", message, publishOptions )
 .then( function( result ) {
    console.log( "message has been published" );
  })
 .catch( function( error ) {
    console.log( "error publishing the message - " + error );
  })
String message = "Thunderstorms likely. High 62F. Winds NNW at 10 to 15 mph. Chance of rain 90%.";
PublishOptions publishOptions = new PublishOptions();
publishOptions.putHeader( "city", "Dallas" );
publishOptions.putHeader( "state", "Texas" );
publishOptions.putHeader( "temperature", 62);

Backendless.Messaging.publish( "demo", 
                               message,
                               publishOptions, 
                               new AsyncCallback<MessageStatus>()
{
  @Override
  public void handleResponse( MessageStatus response )
  {
    Log.i( "MYAPP", "Message has been published" );  
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "Error publishing the message" );
  }
} );
let message = "Thunderstorms likely. High 62F. Winds NNW at 10 to 15 mph. Chance of rain 90%."
let publishOptions = PublishOptions()
publishOptions.addHeader("city", value: "Dallas")
publishOptions.addHeader("state", value: "Texas")
publishOptions.addHeader("temperature", value: 62)
        
backendless.messaging.publish("demo",
                              message: message,
                              publishOptions: publishOptions,
                              response: { response in
                                print("Message has been published")
}, error: { fault in
    print("Error: \(fault?.message ?? "")")
})
NSString *message = @"Thunderstorms likely. High 62F. Winds NNW at 10 to 15 mph. Chance of rain 90%.";
    PublishOptions *publishOptions = [PublishOptions new];
    [publishOptions addHeader:@"city" value:@"Dallas"];
    [publishOptions addHeader:@"state" value:@"Texas"];
    [publishOptions addHeader:@"temperature" value:@62];
    
    [backendless.messaging publish:@"demo"
                           message:message
                    publishOptions:publishOptions
                          response:^(MessageStatus *response) {
                              NSLog(@"Message has been published");
                          }
                             error:^(Fault *fault) {
                                 NSLog(@"Error: %@", fault.message);
                             }];

Sending Email

Backendless provides API for email delivery on behalf of your application. Before the API can be used, the Backendless backend must be configured with your own SMTP server information. This is an important requirement as the API will not work if the Manage > App Settings > Email Settings section in Backendless Console contains default values.

 

Configuration


To configure the backend:

 

1. Login to Backendless Console.
2. Select an app.
3. Click Manage, then scroll down to Email Settings on the App Settings screen.
4. Fill out the form with the SMTP server information
email-settings.zoom50

 
where:

SMTP Server - Hostname or public IP address of the server where the SMTP server is running.
Port - The port number the SMTP server accepts requests on.
From - The Name which will appear in the From field of the sent out emails.
User ID - The user id or email address for the SMTP server connection authentication
Password - The password for the SMTP server connection authentication.
Security - Choose between SSL or TLS connection.

 

Make sure to click Test before saving any configuration changes. The Reset button discards any unsaved changes.

 

Sending Email API


Delivers an email message using current server-side email settings to the recipient specified in the API call.

 

// ALL methods accessible via the Backendless.Messaging construct:

// synchronous methods
public void SendTextEmail( String subject, String messageBody, List<String> recipients );
public void SendTextEmail( String subject, String messageBody, String recipient );
public void SendHTMLEmail( String subject, String messageBody, List<String> recipients );
public void SendHTMLEmail( String subject, String messageBody, String recipient );
public void SendEmail( String subject, BodyParts bodyParts, String recipient, List<String> attachments );
public void SendEmail( String subject, BodyParts bodyParts, String recipient );
public void SendEmail( String subject, BodyParts bodyParts, List<String> recipients, List<String> attachments );

// asynchronous methods
public void SendTextEmail( String subject, String messageBody, List<String> recipients, AsyncCallback<object> responder );
public void SendTextEmail( String subject, String messageBody, String recipient, AsyncCallback<object> responder );
public void SendHTMLEmail( String subject, String messageBody, List<String> recipients, AsyncCallback<object> responder );
public void SendHTMLEmail( String subject, String messageBody, String recipient, AsyncCallback<object> responder );
public void SendEmail( String subject, BodyParts bodyParts, String recipient, List<String> attachments, AsyncCallback<object> responder );
public void SendEmail( String subject, BodyParts bodyParts, String recipient, AsyncCallback<object> responder );
public void SendEmail( String subject, BodyParts bodyParts, List<String> recipients, List<String> attachments, AsyncCallback<object> responder );

where:

subject - email message subject.
messageBody - plain text or HTML body of the email message.
bodyParts - an instance of com.backendless.messaging.BodyParts class which contains either plain text and/or HTML version of the message body.
recipient - email address to deliver the email message to.
recipients - a collection of email addressed to deliver the email message to.
attachments - an array of file paths for the file entries from the Backendless File Service. Referenced files will be attached to the email message. The path is calculated from the root of the file system (as it is seen in File Service browser in Backendless console) without the leading slash. For example, if file agreement.txt is located at /documents/legal/, then the path in the API call must be "documents/legal/agreement.txt".
responder - the callback used for asynchronous calls to indicate that the operation has either successfully completed or resulted in error.

 

Example:

AsyncCallback<object> responder = new AsyncCallback<object>(
  result =>
  {
    System.Console.WriteLine( "[ASYNC] message sent" );
  },

  fault =>
  {
    System.Console.WriteLine( "Error - " + fault );
  } );

// async request. Plain text message to one recipient
Backendless.Messaging.SendTextEmail( "Reminder", "Hey JB! Your car will be ready by 5pm", "james.bond@mi6.co.uk", responder );

// sync request. HTML messahe to multiple recipients
List<String> recipients = new List<String>();
recipients.Add( "mom@gmail.com" );
recipients.Add( "dad@gmail.com" );

String mailBody = "Guys, the dinner last night was <b>awesome</b>";

Backendless.Messaging.SendHTMLEmail( "Dinner", mailBody, recipients );

System.Console.WriteLine( "[SYNC] email has been sent" );

 

Push Notifications

Push Notifications Overview

Backendless supports push notifications for iOS and Android devices and very soon for web browsers and desktop operating systems. Messages published as push notifications can target either a specific device or a group of devices. Push notification recipients can be grouped by operating system (for example, a message sent to all registered iOS devices) or as a collection of individual registrations. The Backendless Push Notification API supports different types of push notifications - badge updates, alerts, etc.

Push Notification Setup (Android)

Backendless can deliver published messages as push notifications to Android devices. Additionally, Backendless Console can be used to publish push notifications. In order to deliver a push notification to Android, the backend must be configured with Google API Key:

2. Click the Get started button and select or create a project.
3. Click the Add Firebase to your Android app button:
add-fb-to-app.zoom70
 
4. Fill out the form by adding the Android package name and click the REGISTER APP button:
package-name-register-app.zoom70
 
5. The steps for adding google-services.json and Firebase SDK to your project can be skipped.
6. Click the gear/settings icon located next to the Project Overview menu item and select Project Settings.
firebase-settings
7. Click the CLOUD MESSAGING tab:
firebase-cloud-messaging.zoom70
8. The Cloud Messaging tab displays project credentials. Make sure there is a Server Key generated for the project. Copy Server key: (you can use either Server key or Legacy server key):
firebase-server-key.zoom70
 
9. Open Backendless Console and select your application.
10. Click Manage and scroll down to Mobile Settings.
11. Paste the Google Server Key into corresponding field located in the Android Server Keys sections:
adding-android-key.zoom50
12. Click Save. At this point the backend is configured and is ready to publish push notifications to Android devices.

 

Google Project Number (Sender ID)

In your project you should register the device in order to receive or send push notifications. To accomplish this, do the following:

1. Return to Step 8 in the instructions above.
2. Copy Sender ID.
3. Use the Sender ID value in Backendless.Messaging.registerDevice(...) method as GCMSenderID argument. For example:
google-project-number-in-code

Manifest Configuration

Android applications must also include special configuration into the application manifest file to enable support for Backendless Push Notifications:

1. Add the following permissions:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="[APP PACKAGE NAME].permission.C2D_MESSAGE"/>
<permission android:name="[APP PACKAGE NAME].permission.C2D_MESSAGE" 
               android:protectionLevel="signature"/>

where [APP PACKAGE NAME] is the top level package name, it must be the same value as the package attribute in the manifest root element.

2. Add receiver declaration into the <application> element in the manifest file:
<receiver android:name="com.backendless.push.BackendlessBroadcastReceiver" 
             android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
    <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
    <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
    <category android:name="[APP PACKAGE NAME]"/>
  </intent-filter>
</receiver>
<service android:name="com.backendless.push.BackendlessPushService" />

where [APP PACKAGE NAME] is the top level package name, it must be the same value as the package attribute in the manifest root element.

Receiver is responsible for processing incoming push notifications.

 

Notice the value for the android:name attribute in the <receiver> and <service> elements - The receiver class is com.backendless.push.BackendlessBroadcastReceiver. While the service is com.backendless.push.BackendlessPushService. These are the default implementations. They contain the logic for processing push notifications and displaying them in the device's Notification Center. The implementation can be extended in order to get access to the published messages and/or handle push notifications in a custom way.

 

Custom handling of push notifications requires an implementation of two classes. First create a class which extends com.backendless.push.BackendlessPushService. Your implementation can override each of the four methods which correspond to various GCM actions.

Notice the comment in the onMessage method. The implementation of the method in the BackendlessPushService class contains the logic of displaying push notifications in the Android Notification Center. To cancel the default handling of the push notification, make sure your code returns false.

package com.myPackage;

import com.backendless.push.BackendlessPushService;

public class MyPushService extends BackendlessPushService
{

  @Override
  public void onRegistered( Context context, String registrationId )
  {
    Toast.makeText( context, 
                    "device registered" + registrationId, 
                    Toast.LENGTH_SHORT).show();
  }

  @Override
  public void onUnregistered( Context context, Boolean unregistered )
  {
    Toast.makeText( context, 
                    "device unregistered", 
                    Toast.LENGTH_SHORT).show();
  }

  @Override
  public boolean onMessage( Context context, Intent intent )
  {
    String message = intent.getStringExtra( "message" );
    Toast.makeText( context, 
                    "Push message received. Message: " + message, 
                    Toast.LENGTH_LONG ).show();
    // When returning 'true', default Backendless onMessage implementation 
    // will be executed. The default implementation displays the notification 
    // in the Android Notification Center. Returning false, cancels the 
    // execution of the default implementation.
    return false;
  }

  @Override
  public void onError( Context context, String message )
  {
    Toast.makeText( context, message, Toast.LENGTH_SHORT).show();
  }
}

Once a custom service class is defined, it has to be registered with GCM. Backendless handles it through a custom implementation of broadcast receiver. Create a class that extends com.backendless.push.BackendlessBroadcastReceiver. Your implementation must override the getServiceClass() method which returns the Class object of the service implementation:

package com.myPackage;

import com.backendless.push.BackendlessBroadcastReceiver;
import com.backendless.push.BackendlessPushService;

public class MyPushReceiver extends BackendlessBroadcastReceiver
{
  @Override
  public Class<? extends BackendlessPushService> getServiceClass()
  {
    return MyPushService.class;
  }
}
. Make sure to modify the manifest file to reflect your class implementations:
<receiver android:name=".MyPushReceiver" 
             android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
    <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
    <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
    <category android:name="com.myPackage"/>
  </intent-filter>
</receiver>
<service android:name=".MyPushService" />

 

Push Notification Setup (iOS)

Setting up your backend to support Push Notifications for iOS requires a few steps, most of which are in Apple Developer Member Center and Keychain Access.  The process consists of the following steps:

 

 

Creating App ID

1. First we are going to create an App ID for the mobile application which will receive Push Notifications. Login to Apple Developer Member Center. Click on "App IDs" in the "Identifiers" section. Use the plus sign "+" button to create a new ID:
create-app-id.zoom60
2. When prompted enter App ID Prefix. Make sure it is descriptive enough so you recognize it later when you return to the Member Center.
3. Select Explicit App ID in the "App ID Suffix" section and enter the same bundle ID which you will be using in the application:
appidsuffix.zoom95
4. In App Services select the services which the application will use and click "continue":
appservices.zoom95
5. Make sure that Push Notifications are enabled and click "submit". This will conclude the App ID creation for the app:
confirm-app-id-creation.zoom90

 

Creating Certificate Request

Push Notifications require a certificate which will be used on a device by the means of a provisioning profile. Also the same certificate (transformed to the Personal Information Exchange - .p12 format) will be used by Backendless to publish Push Notifications. If this makes little sense, do not worry, you will need to perform these steps only ones and then can move on to code and using the APIs.

1. In order to create a certificate a Certificate Signing Request (CSR) must be issued. To create a CSR, open Keychain Access and select Keychain Access >> Certificate Assistant >> Request a Certificate from the main menu:
certificaterequest.zoom80
2. Enter your email address and Common Name (leave the CA Email Address field empty), select "Saved to disk" and click "Continue":
CertificateRequestData
3. Select a directory where to save the file and click Save.

 

Generating an SSL Certificate

The CSR file created in the section above will be used to create an SSL Certificate. That certificate will then be used by Backendless to publish push notifications.

1. Return to Apple Developer Member Center and select "All" under "Certificates". Click the plus button "+" to add a new certificate:
membercenteraddcertificate.zoom60
2. Select certificate type - there are two options Development and Production. For now select "Apple Push Notification service SSL (Sandbox)":
push-cert-sandbox.zoom90
3. Select the App ID created earlier in these instructions:
SelectAppId
4. Next you will see the instructions for generating a CSR which you have already created by now. Click Continue to proceed to the next step.
5. Select the CSR file created and saved to the disk earlier and click Generate:
select-csr-file
6. The certificate is ready now, click "Download" to download it:
download-push-cert.zoom90
7. Add the certificate file to Keychain Access.
8. Open Keychain Access and locate the certificate in the "My Certificates" section:
locate-push-cert-keychain-access.zoom60
9. Right click on the certificate and select the Export option:
export-push-cert.zoom70
10. Save the certificate in the p12 format:
save-cert-p12-format
11. Enter a password for the certificate. Make sure to make a record of the password - you will need to use it later in the instructions when you submit the certificate to Backendless:
enter-password
12. Enter your Mac OS X account password to confirm the action. At this point you have a certificate for Push Notifications.

 

Configuring Backendless App/Backend with the Certificate

Since Backendless provides the actual server-side integration for delivering Push Notifications for your application, it needs to have access to the certificate you created above. The steps below provide the instructions for uploading the certificate into Backendless:

1. Login to Backendless Console and select an application which you will use on the server-side.
2. Click Manage > App Settings. Locate the Mobile Settings section and click the IOS menu.
3. Click the Add Certificate button and upload the .p12 certificate created earlier. Make sure to enter the same password you used when created the certificate. The Channels control lets you select the messaging channels which the certificate will be used with. If you intend to use only one certificate, click the Add channels checkbox.
add-ios-cert
4. Now your Backendless server is ready to publish Push Notifications.

Device Registration

In order to receive push notifications from Backendless, a device must be registered using the API documented below. Device registration may optionally include a list of messaging channels and/or an expiration date/time when the registration should be canceled.

 

Push notifications can be published using either API or Backendless Console. When a push notification is published, it goes through a messaging channel. Channels provide a way to establish a level of filtering - devices registered with a channel will receive notifications published to the channel. However, there are other way to narrow down push notification delivery. For example, a push notification may be sent to a specific device or a group of devices.

 

If no channels are specified in the device registration call, Backendless registers the device with the default channel. If the device registration call references a non-existing channel, Backendless creates the channel and registers the device with it. Registration expiration is a point in time (expressed as a timestamp) when the device registration must expire. Backendless removes the device registration at the specified time and the device no longer receives published push notifications.

 

In order to receive push notifications on a mobile device, application running on the device must register with Backendless using the API call below:

 

// register device with the "default" channel
public void registerDevice( String GCMSenderID )

// register device with the specified channel
public void registerDevice( String GCMSenderID, String channel )

// register device with the channels and assign expiration timestamp for the registration
public void registerDevice( String GCMSenderID, List<String> channels, Date expiration )

where:

gcmSenderID -  Google application project number. See the See the Google Project Number section in the Push Notification Setup section of the guide for information on how to obtain the GCM Sender ID
channel(s) - channel (or a collection of channels) to receive messages from. If a value is not provided, Backendless  registers the device with the default channel;
expiration -  a timestamp when the device registration should expire.

Errors:

 

The following errors may occur during the message publishing API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

Error Code

Description

5004

Invalid expiration date. The expiration date must be after the current time.

8000

Property value exceeds the length limit. Error message should contain additional details about the violating property.

Example:

// do not forget to call Backendless.initApp in the app initialization section 
Backendless.Messaging.registerDevice( gcmSenderID );

 

Retrieve Device Registration

Backendless server returns information about device registration with the API documented below. The input parameter of the API is deviceId - the value returned by the device registration API.

Blocking Method:

public DeviceRegistration getDeviceRegistration() throws BackendlessException;

Non-blocking Method:

public void getDeviceRegistration( AsyncCallback<DeviceRegistration> responder );

Return value:

DeviceRegistration - the AsyncCallback argument of the asynchronous method receives an instance of DeviceRegistration in the handleResponse method.

Errors:

The following errors may occur during the device registration API callThe following errors may occur during the device registration API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

Error Code

Description

5000

Unable to retrieve device registration - unknown device ID.

Examples:

// do not forget to call Backendless.initApp in the app initialization code

DeviceRegistration devReg = Backendless.Messaging.getDeviceRegistration();

 

Managing Registrations

Application developers can manage device registrations using the Backendless Console:

1. Login to Backendless Console and select an app where you would like to see the device registrations.
2. Click the Data icon and select the DeviceRegistration table.
devicereg-table.zoom50

 

The table provides a single, unified view of all devices registered to receive push notifications. The table contains important information such as deviceId, deviceToken, a name of the messaging channel (channelName). Since the data about registered devices is stored directly in the Backendless database, it automatically supports the following functions:

Registered devices can be exported/imported into Backendless apps using the export/import functionality of the Data Service.
Data relations between registered devices and other data objects and geopoints (and vice versa)
Devices can be registered using the Data Service API or directly via the console.
SQL (whereClause-based) searches of the devices using the console or the Data Service API.

 

Backendless automatically links registered device with the currently logged in user. Suppose your app registers the device for push notifications. If there is a logged in user at the time when the device is being registered, Backendless creates a relation between the device and the user in the database (via a 1:1 relation):

device-to-user-mapping.zoom70

Publish Push Notifications

Publish Push Notifications Overview

Backendless supports push notification delivery to iOS, Android and Windows Phone devices. A device must be registered (see the Device Registration API) in order to receive a push notification. A push notification can be published via API or using the Backendless Console. The content of a push notification consists of a message body and the specialized message headers.

Push from Console

To publish a push notification from Backendless Console, navigate to the Messaging screen and click the Devices tab. Make sure there are registered devices, otherwise a push notification has nowhere to go.

1. Select the check boxes for the device(s) you would like to send a notification to. Select Android or iOS tab to enter push notification parameters.
2. Enter parameter values into the corresponding fields.
3. Click the Publish Message button to send a push notification.
messaging-screen-v4.zoom50

Push Notification Headers

The headers described below must included with every push notification, when using the Push API.

Operating System

Headers

Description

iOS

"ios-alert-title":value

Sets the title of the push notification message.

"ios-alert-subtitle":value

Sets the sub-title of the push notification message (iOS 10+)

"ios-alert-body":value

Sets the text of the alert message. If the header is not present and the published notification targets the iOS devices, Backendless sets the header to the value of the "message" argument.

"ios-badge":value

Sets the value to update the badge with

"ios-sound":URL string or array of bytes

Sets either a URL for the sound notification to play on the device or an array of bytes for the sound to play.

"ios-content-available":value

Configures a silent-notification. When this key is present, the system wakes up your app in the background and delivers the notification to its app delegate.

Android

"android-ticker-text":value

Sets the text of the ticker showing up at the top of a device's screen when the device receives the notification.

"android-content-title":value

Sets the title of the notification as it is visible in the Android Notification Center

"android-content-text":value

Sets the message of the notification which appears under android-content-title in the Android Notification Center.

Push with API

Message publishing API supports the following scenarios:

 

Basic push notification - In the simplest form a push notification is sent to a channel and thus to all the devices registered with the channel. The notification message must contain headers which control the appearance of the notification on the devices. Example.

 

Push notification delivered to devices grouped by the OS - With this type of push notifications delivery, a notification can be sent to all devices of a specific operating system (or a combination of).  This includes delivery to Android, iOS or both. Example.
 
Push notification delivered to specific devices - This delivery type allows push notification delivery to individual devices. Example
 
Delayed publishing - For all scenarios described above a push notification request can be configured to be executed at a later time. Example.

Blocking Methods

Publishes push notification to the "Default" channel. Headers must be added to the publishOptions argument.

public MessageStatus Backendless.Messaging.publish( Object message, 
                                                    PublishOptions publishOptions ) 
                                                        throws BackendlessException

 

Same as above, but the notification is published into the specified channel. Headers must be added to the publishOptions argument.

public MessageStatus Backendless.Messaging.publish( String channelName, 
                              Object message, 
                              PublishOptions publishOptions ) 
                                       throws BackendlessException

Publishes push notification to the "Default" channel. Headers must be added to the publishOptions argument. Delivery options can be used to:

1. Designate the message as "push notification only" or both a push notification and a pub/sub message.
2. Singlecast publishing - set a list of devices which should be receiving the notification.
3. Broadcast publishing - identify operating systems of the registered devices which should be receiving the notification.
4. Publish the message at a later time (delayed publishing).
5. Specify that the message should be republished multiple times (repeated publishing).

public MessageStatus Backendless.Messaging.publish( Object message, 
                              PublishOptions publishOptions, 
                              DeliveryOptions deliveryOptions ) 
                                     throws BackendlessException

Same as above, but published into the specified channel.

public MessageStatus Backendless.Messaging.publish( String channelName, 
                              Object message, 
                              PublishOptions publishOptions, 
                              DeliveryOptions deliveryOptions )  
                                     throws BackendlessException

Non-blocking Methods:

Same set of methods as above, but executed asynchronously. The difference in the method signatures is the AsyncCallback<MessageStatus> argument:

public void Backendless.Messaging.publish( Object message, 
                                           PublishOptions publishOptions, 
                                           AsyncCallback<MessageStatus> responder )

public void Backendless.Messaging.publish( String channelName, 
                                           Object message, 
                                           PublishOptions publishOptions, 
                                           AsyncCallback<MessageStatus> responder )

public void Backendless.Messaging.publish( Object message, 
                                           PublishOptions publishOptions, 
                                           DeliveryOptions deliveryOptions, 
                                           AsyncCallback<MessageStatus> responder )

public void Backendless.Messaging.publish( String channelName, 
                                           Object message, 
                                           PublishOptions publishOptions, 
                                           DeliveryOptions deliveryOptions, 
                                           AsyncCallback<MessageStatus> responder )

where:

channelName - name of the channel to publish the notification to. If the channel does not exist, Backendless automatically creates it.
message - object to publish. The object can be of any data type - a primitive value, String, Date, a  user-defined complex type, a collection or an array of these types.
publishOptions - an instance of PublishOptions. When provided may contain publisher ID (an arbitrary, application-specific string value identifying the publisher), and/or a collection of headers.
deliveryOptions - an instance of DeliveryOptions. When provided may specify options for push notification delivery such as: deliver to specific devices (or devices grouped by the operating system), delayed delivery or repeated delivery.

 

Return value:

MessageStatus - a data structure which contains ID of the published message. Use the getMessageId() method to get the ID assigned to the message by the backend (the ID is needed to check the message publishing  status).

Errors:

The following errors may occur during the message publishing API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

Error Code

Description

5003

Invalid repeatExpiresAt date in delivery options.

5007

User does not have the permission to publish messages

5030

Invalid publishAt date in the delivery options.

Get Message Status

Backendless processes push notification delivery asynchronously. Even when the client app uses blocking/synchronous API, the server creates a push notification delivery task and adds to a queue. Server assigns an ID to the task and returns it to the client as messageId. As a result, push notification delivery status is not available right away. To retrieve the status, the client application must make a separate API call documented below:

Synchronous API:

Backendless.Messaging.getMessageStatus( String messageId );

Asynchronous API:

Backendless.Messaging.getMessageStatus( String messageId, AsyncCallback<MessageStatus> responder );

where:

messageId        -  ID of the message assigned to the message and returned by the Message Publishing API request.

Return Value:

Method returns an instance of com.backendless.messaging.MessageStatus. The object contains the following properties:

messageId        - ID of the message for which the publishing status is retrieved.

status                - an instance of an enum type com.backendless.messaging.PublishStatusEnum.

errorMessage        - contains detailed error message when status is PublishStatusEnum.FAILED.

 

Example: Basic Push

// do not forget to call Backendless.initApp in the app initialization code

PublishOptions publishOptions = new PublishOptions();
publishOptions.putHeader( "android-ticker-text", "You just got a push notification!" );
publishOptions.putHeader( "android-content-title", "This is a notification title" );
publishOptions.putHeader( "android-content-text", "Push Notifications are cool" );
MessageStatus status = Backendless.Messaging.publish( (Object) "Hi Devices!", 
                                                                publishOptions );

 

Example: Target All Devices for an OS

// do not forget to call Backendless.initApp in the app initialization code

DeliveryOptions deliveryOptions = new DeliveryOptions();

deliveryOptions.setPushBroadcast( PushBroadcastMask.ANDROID | PushBroadcastMask.IOS );

PublishOptions publishOptions = new PublishOptions();
publishOptions.putHeader( "android-ticker-text", "You just got a push notification!" );
publishOptions.putHeader( "android-content-title", "This is a notification title" );
publishOptions.putHeader( "android-content-text", "Push Notifications are cool" );

MessageStatus status = Backendless.Messaging.publish( "Hi Devices!", 
                                                      publishOptions, 
                                                      deliveryOptions );

 

Example: Target Individual Devices

// do not forget to call Backendless.initApp in the app initialization code 
DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.addPushSinglecast( Arrays.asList( receiver-device-id ) );

PublishOptions publishOptions = new PublishOptions();
publishOptions.putHeader( "android-ticker-text", "You just got a private push notification!" );
publishOptions.putHeader( "android-content-title", "This is a notification title" );
publishOptions.putHeader( "android-content-text", "Push Notifications are cool" );

MessageStatus status = Backendless.Messaging.publish( "this is a private message!", 
                                                      publishOptions, 
                                                      deliveryOptions );

 

Example: Delayed Push

// do not forget to call Backendless.initApp in the app initialization code

PublishOptions publishOptions = new PublishOptions();
publishOptions.putHeader( "android-ticker-text", "You just got a private push notification!" );
publishOptions.putHeader( "android-content-title", "This is a notification title" );
publishOptions.putHeader( "android-content-text", "Push Notifications are cool" );

DeliveryOptions deliveryOptions = new DeliveryOptions();
Date publishDate = new Date( System.currentTimeMillis() + 20000 ); // add 20 seconds
deliveryOptions.setPublishAt( publishDate );

MessageStatus status = Backendless.Messaging.publish( "This message was scheduled 20 sec ago", 
                                                      publishOptions, 
                                                      deliveryOptions );

 

Cancel Device Registration

To cancel the registration of a device with Backendless, an application can use the API described below:

 

public void unregisterDevice() throws BackendlessException;

Errors:

The following errors may occur during the device registration cancellation API callThe following errors may occur during the device registration API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

Error Code

Description

5001

Unable to cancel device registration - unknown device ID.

Example:

String applicationID = "YOUR-APP-ID";
String secretKey = "ANDROID-KEY";
String version = "v1";
// this call must be somewhere early on in the application initialization
Backendless.initApp( appContextOrActivity, applicationId, secretKey, version );

try
{
  Backendless.Messaging.unregisterDevice();
}
catch( BackendlessException exception )
{
  // either device is not registered or an error occurred during de-registration
}

 

Files API

Files API Overview

Every Backendless backend/app is allocated a dedicated file storage space. The file storage is located remotely on the Backendless servers. The file storage can be used to store application's files and on-demand video streams.  Backendless File Service provides the API to work with the file storage. The API supports the following operations:

 

File Upload - upload files to the applications's file storage. The operation creates directories up the hierarchy if necessary. Returns file URL which can be used to download or share the file with others.
File Download - download file using file's URL. The download operation is subject to the permissions from the File access control list (ACL).
File Deletion - delete a file from the file storage. The delete operation is subject to the permissions from the File access control list (ACL).
Directory Deletion - same as file deletion, but applies to the directories.
File/Directory Security (File ACL) - assign/unassign user and roles permissions to upload, download and delete files and directories. This API is used to modify file or directory ACL.

 

In addition to the API implementation, the File Service enables the following capabilities:

 

Git Integration - application developers can interact with the file storage as with a git repository.
Web Hosting - file storage can be used to host static web content.
Custom Domain Name - a custom domain name can be mapped to the file storage in a Backendless backend. This feature in combination with the Web Hosting provides a way to host websites on Backendless.
Custom Web Templates Hosting - includes HTML files and JS scripts for special pages used in various workflows such as user email confirmation, password change and session expiration.

Handling Files in Console

Backendless Console includes a graphical file browser which supports the following operations:

Creating a New File

To create a file:

1. Log in to Backendless Console and select an application. Click the Files icon:
files-screen1.zoom50
2. Select a directory where a new file should created (or create a new directory). Click the New File link from the toolbar.
3. Enter a name in the File name field. Additionally, you can select syntax highlighter from the menu located in the lower-right corner. The highlighter selection is available only for new files. When an existing file is opened for editing, a highlighter is automatically selected based on the file extension.
files-screen2.zoom50
4. To save the file click the Save or the Save and Close button.

Editing a File

To edit a file:

1. Select a directory containing the file on the Files screen of the console.
2. Click the Edit file icon in the Actions column for the file to open it for editing:
files-screen3
3. Once the changes in the file are made click the Save or the Save and Close button.

Getting Public URL for a File

A file in the Backendless File Storage has two URLs:

a public URL which can be used to download the file outside of Backendless console. This URL is subject for any permissions assigned to the file or the directory where it resides.
a private URL which makes the file accessible to the developer of the application.

In order to obtain the public URL:

1. Select a directory containing a file on the Files screen of Backendless console.
2. Click the Get Public URL icon next to the file. Backendless console copies the file's public URL to the computer's clipboard:
files-screen4

To get file's private URL:

Right click file name and select "Copy Link Address" (text of the menu may vary depending on the browser):
files-screen5

File's private URL works only if the request to download the file contains the auth-key HTTP header. The value of the header is a token assigned by Backendless to the developer login to console.

Archiving Files

Backendless Console includes a feature enabling to compress directories into a single ZIP file. The feature applies specifically to directories, meaning an individual file cannot be compressed - it must be placed into a directory first.

Notice: archiving of directories with total content size greater than 100 Mb may take longer time; Backendless sends an email to the application developer upon successful completion of the operation.

To archive a directory:

1. Log in to Backendless Console, select an application and click the Files icon.
2. Navigate to a directory which should be compressed.
3. Click the ZIP Directory button:
files-screen6
4. Once the directory is compressed into an archive, it will appear in the parent directory:
files-screen7

File Upload

The file upload operation delivers and saves a local file in the remote Backendless file storage. The return value of the operation is the file URL which has the following structure:

https://api.backendless.com/<application id>/<REST-api-key>/files/<path>/<file name>

where:

<application id> - ID of the application which can be obtained from the Manage > App Settings screen of the Backendless Console
<REST-api-key> - REST API key of your application. You can get the value from the Manage > App Settings section of the Backendless Console.
<path> - directory path where the file is saved
<file name> - name of the file

The URL assigned to a file and returned as a result of the upload operation accounts for any security permissions assigned to the file (or the folder it is located in).

Upload a file to a remote path in the Backendless storage.

public void BackendlessAPI.Backendless.FilesUpload( FileStream fileStream, 
                                                    string remotePath, 
                                                    AsyncCallback&ltBackendlessFile callback );

Upload a file to a remote path and receive updates for the status of the upload. Updates are represented as percentages and are delivered via the uploadCallback argument.

public void BackendlessAPI.Backendless.FilesUpload( FileStream fileStream, 
                                                    string remotePath, 
                                                    UploadCallback uploadCallback, 
                                                    AsyncCallback&ltBackendlessFile callback )

where:

fileStream - stream for a local file to upload to Backendless file storage. Remote file name is the same as the name of the local file.
remotePath - path (without file name) in the remote Backendless file storage where the file should be saved.
uploadCallback - receives updates through the upload process. Updates expressed as percentage of file uploaded to the server.
callback - a responder object which receives a callback when the method successfully uploads the file or if an error occurs.

Return Value (delivered via AsyncCallback):

BackendlessFile - a wrapper around the URL assigned to the uploaded file.

Additional API Classes:

hmtoggle_plus1        UploadCallback

namespace BackendlessAPI.Async
{
  public delegate void ProgressHandler( int response );

  public class UploadCallback
  {

    internal readonly ProgressHandler ProgressHandler;

    public UploadCallback( ProgressHandler progressHandler )
    {
      ProgressHandler = progressHandler;
    }
  }
}

hmtoggle_plus1        BackendlessFile

using BackendlessAPI.Async;

namespace BackendlessAPI.File
{
  public class BackendlessFile
  {
    public BackendlessFile( string fileURL )
    {
      FileURL = fileURL;
    }

    public string FileURL { get; set; }

    public void Remove()
    {
      Backendless.Files.Remove( FileURL );
    }

    public void Remove( AsyncCallback&ltobject> callback )
    {
      Backendless.Files.Remove( FileURL, callback );
    }
  }
}

Example:

AsyncCallback<BackendlessAPI.File.BackendlessFile> callback = new AsyncCallback<BackendlessAPI.File.BackendlessFile>(
        result =>
        {
          System.Console.WriteLine( "File uploaded. URL - " + result.FileURL );
        },

        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

FileStream fs = new FileStream( "test.txt", FileMode.Open, FileAccess.Read );
BackendlessAPI.Backendless.Files.Upload( fs, "myfiles", callback );

 

Save Files From Byte Arrays

In addition to the classic file upload, files can be saved by uploading a byte array which becomes the content of the saved file.

Methods:

Synchronous methods:

public String SaveFile( String filePathName, byte[] fileContent )
public String SaveFile( String filePathName, byte[] fileContent, Boolean overwrite )
public String SaveFile( String path, String fileName, byte[] fileContent )
public String SaveFile( String path, String fileName, byte[] fileContent, Boolean overwrite )

Asynchronous methods:

public void SaveFile( String filePathName, byte[] fileContent, AsyncCallback<String> responder )
public void SaveFile( String filePathName, byte[] fileContent, Boolean overwrite, AsyncCallback<String> responder )
public void SaveFile( String path, String fileName, byte[] fileContent, AsyncCallback<String> responder )
public void SaveFile( String path, String fileName, byte[] fileContent, Boolean overwrite, AsyncCallback<String> responder )

where:

filePathName - Identifies both the directory where the file should be saved and the file name. Must start with "/" which represents the root directory of the remote file storage.
path - Path of the directory where the file should be stored. Must start with "/" which represents the root directory of the remote file storage.
fileName - Name of the file where the byte content should be written to.
fileContent - An array of bytes to save.
overwrite - The file is overwritten if the argument value is true and the file already exists. Otherwise, if the value is false and another file with the same name already exists, an error  is returned.
responder - A responder object which receives a callback when the method successfully completes or if an error occurs. Applies to the asynchronous methods only.

Return Value (delivered via AsyncCallback):

BackendlessFile - A wrapper around the URL assigned to the uploaded file.

Additional API Classes:

hmtoggle_plus1        BackendlessFile

using BackendlessAPI.Async;

namespace BackendlessAPI.File
{
  public class BackendlessFile
  {
    public BackendlessFile( string fileURL )
    {
      FileURL = fileURL;
    }

    public string FileURL { get; set; }

    public void Remove()
    {
      Backendless.Files.Remove( FileURL );
    }

    public void Remove( AsyncCallback<object> callback )
    {
      Backendless.Files.Remove( FileURL, callback );
    }
  }
}

Example:

The example below describes how to save a file called "fox.txt" from the string "The quick brown fox jumps over the lazy dog." You will need to specify:

content of a new file ("The quick brown fox jumps over the lazy dog")
where to save the new file ("testfolder")
a name of the newly created file ("fox.txt")
whether a new file should overwrite the existing file, if any (true)

static void SaveFile()
{
   String content = "The quick brown fox jumps over the lazy dog";
   byte[] bytes = UTF8Encoding.UTF8.GetBytes( content );
   String savedFileURL = Backendless.Files.SaveFile( "tempfolder", "fox.txt", bytes, true );
   System.Console.WriteLine( "File saved. File URL - " + savedFileURL );
}

The server will return notification and link to the newly added file ("File saved. File URL - ") or an error.

Errors:

Error codes returned on attempt to save a file from the byte array.

Error Code

Description

6016

When saving a new file from the byte array, the payload exceeds 2,800,000 bytes.

6003

A file you are trying to save already exists in the system and cannot overwrite since overwrite argument is ether set to false or omitted.

 

File Download

Downloading a File via the Backendless Console

To download a file:

1. Log in to Backendless Console and select the application containing the file.
2. Click the Files tab on the left menu.
3. Locate a file you want to download. Click the Download file icon.

files-screen8

Downloading a File via API

Downloading a file from the Backendless file storage is the basic HTTP GET operation. The operation should use the same URL which Backendless returned as the result of the file upload operation. Alternatively, if the file was uploaded manually using the console, the URL can be composed as:

https://api.backendless.com/<application id>/<REST API key>/files/<path>/<file name>

where:

<application id> - ID of the application which can be obtained from the Manage > App Settings screen of the Backendless Console.
<REST API key> - REST API key assigned to the application by Backendless. The key is available from the Manage > App Settings screen of the Backendless Console.
<path> - Directory path where the file is saved.
<file name> - Name of the file.

Files fetched with the URL scheme defined above are subject to the security constraints and permissions established by the application developer. See the Files Security section for additional details on how to secure file storage. Fetching a file secured by an access control list (ACL) policy requires an additional HTTP header in the request:

user-token:<value>

where:

<value> - a value identifying currently logged in user (if any). This is the value received in the response for the login API request.  The token uniquely identifies the user and the roles associated with him. It is used by Backendless to establish user's identity for all operations where the token is present. It is necessary in order to determine permissions applicable to the user and the roles associated with the account. This header is optional.

Renaming a File/Directory

 
Methods:
Synchronous method:

public string RenameFile( String oldPathName, String newName )

Asynchronous method:

public void RenameFile( String oldPathName, String newName, AsyncCallback<string> responder )

where:

oldPathName a path identifying file or directory to be renamed. The must start with the root directory for the file storage allocated to the application.
newName - new name for the file or directory.
responder - A responder object which receives a callback when the method successfully completes or if an error occurs. Applies to the asynchronous methods only.

Return value:

Absolute path of the renamed file or directory.

Example:

The example below describes how to rename a file called readme.txt located in the /documentation directory to readme-first.txt:

Backendless.Files.RenameFile( "/documentation/readme.txt", "readme-first.txt" );

Errors:

The server may return the following errors:

Error Code

Error message

Notes

4000

User has no permissions to specified resource

Occurs when the user (anonymous or logged in) has no permission to modify the file

6000

File or directory cannot be found.

Occurs when the source file/directory cannot be found.

6006

Path contains prohibited symbols: {path name}


6007

The specified resource was not found

Occurs when the API requests to rename a non-existent file or a directory.

6018

Unable to modify file/folder: file/folder already exists: {file/directory name}

Occurs when the target file/directory already exists.

6028

Parameter {param name} cannot be null

Occurs when one of the parameters is null.

8010

Missing field {field name}

Occurs in REST APIs when one of the required fields is missing in the body.

Copying a File/Directory

Methods:
Synchronous method:

public string CopyFile( String sourcePathName, String targetPath )

Asynchronous method:

public void CopyFile( String sourcePathName, String targetPath, AsyncCallback<string> responder )

where:

sourcePathName - a path identifying file or directory to be copied. The path must start with the root directory of the remote file storage.
targetPath - a path to a directory where the source file or directory should be copied to. If the directory does not exist, it is created.
responder - A responder object which receives a callback when the method successfully completes or if an error occurs. Applies to the asynchronous methods only.

Return value:

Absolute path to the copied file or directory.

Example:

The example below describes how to copy a file called readme.txt located in the /documentation directory to the /updated-docs directory:

Backendless.Files.CopyFile( "/documentation/readme.txt", "/updated-docs" );

Errors:

The server may return the following errors:

Error Code

Error message

Notes

4000

User has no permissions to specified resource

Occurs when the user (anonymous or logged in) has no permission to modify the file

6000

File or directory cannot be found.

Occurs when the source file/directory cannot be found.

6006

Path contains prohibited symbols: {path name}


6007

The specified resource was not found

Occurs when the API requests to rename a non-existent file or a directory.

6018

Unable to modify file/folder: file/folder already exists: {file/directory name}

Occurs when the target file/directory already exists.

6028

Parameter {param name} cannot be null

Occurs when one of the parameters is null.

8010

Missing field {field name}

Occurs in REST APIs when one of the required fields is missing in the body.

8011

Content type should be 'application/json'

Occurs in REST API when the request's content type is not application/json

Moving a File/Directory

Synchronous method:

public string MoveFile( String sourcePathName, String targetPath )

Asynchronous method:

public void MoveFile( String sourcePathName, String targetPath, AsyncCallback<string> responder )

where:

sourcePathName - a path identifying file or directory to move. The path must start with the root directory of the remote file storage.
targetPath - a path to a directory where the source file or directory should be moved to. If the directory does not exist, it is created.
responder - A responder object which receives a callback when the method successfully completes or if an error occurs. Applies to the asynchronous methods only.

Return value:

Absolute path to the copied file or directory.

Example:

The example below describes how to move a file called readme.txt located in the /documentation directory to the /updated-docs directory:

Backendless.Files.MoveFile( "/documentation/readme.txt", "/updated-docs" ); 

Errors:

The server may return the following errors:

Error Code

Error message

Notes

4000

User has no permissions to specified resource

Occurs when the user (anonymous or logged in) has no permission to modify the file

6000

File or directory cannot be found.

Occurs when the source file/directory cannot be found.

6006

Path contains prohibited symbols: {path name}


6007

The specified resource was not found

Occurs when the API requests to rename a non-existent file or a directory.

6018

Unable to modify file/folder: file/folder already exists: {file/directory name}

Occurs when the target file/directory already exists.

6021

Unable to move file to file


6023

File already exists in target directory


6028

Parameter {param name} cannot be null

Occurs when one of the parameters is null.

8010

Missing field {field name}

Occurs in REST APIs when one of the required fields is missing in the body.

8011

Content type should be 'application/json'

Occurs in REST API when the request's content type is not application/json

Directory Listing

Methods:

Synchronous methods:

public BackendlessCollection<FileInfo> Listing( String path );
public BackendlessCollection<FileInfo> Listing( String path, String pattern, bool recursive )
public BackendlessCollection<FileInfo> Listing( String path, String pattern, bool recursive, int pagesize, int offset )

Asynchronous methods:

public void Listing( String path, AsyncCallback<BackendlessCollection<FileInfo>> responder )
public void Listing( String path, String pattern, bool recursive, AsyncCallback<BackendlessCollection<FileInfo>> responder )
public void Listing( String path, String pattern, bool recursive, int pagesize, int offset, AsyncCallback<BackendlessCollection<FileInfo>> responder )

where:

path - path of a directory to get a listing of. The path must start with the root directory identified by forward slash, for example: "/web".
pattern - A pattern which the returned files and directories must match. The pattern can include wildcard characters. Asterisk (*) matches a substring of any length. Question mark (?) matches only one character in the file/directory name.
recursive - an optional parameter. If present and is true, requests that the listing must be retrieved recursively for all directories contained within <path>.
pagesize - an optional parameter. If present, identifies how many items should be returned in the response.
offset - an optional parameter. If present, indicates the index of item in the response from which to get the <pagesize> items.
responder - A responder object which receives a callback when the method successfully completes or if an error occurs. Applies to the asynchronous methods only.

Return value:

A collection of files and directories matching the specified pattern. The returned collection may not include all files and directories as the size of the number of returned entries is limited for paging purposes. To get the next set of file/directory entries, an additional request must be made with a different offset value. The total count of all files/directories in the listing use the Get File Count API.

Each element in the collection contains the following properties:

name - name of the file or directory without any path information
public URL - absolute URL of the file or directory
URL - relative URL of the file or directory starting from the root of the file storage
created on - a timestamp indicating when the file or directory were created

Example:

The example below describes how to get a listing of all HTML files from the /web directory and all subdirectories.

Backendless.Files.Listing( "/web", "*.html", true );

Errors:

The server may return the following errors:

Error Code

Error message

Notes

6000

File or directory cannot be found.

Occurs when the source file/directory cannot be found.

6006

Path contains prohibited symbols: {path name}


6007

The specified resource was not found

Occurs when the API requests to rename a non-existent file or a directory.

6029

Specified resource must be a directory

Occurs when the path to get a listing for is not a directory

Get File Count

This operation returns the number of files and optionally directories located in the specified path. Additional options include:

 

pattern filtering - counts files and directories which match the pattern.
recursive counting - when enabled, counts all matching files and directories while recursively traversing the directory structure.

 

Deleting a File

To delete a file from the Backendless file storage, it must be identified by the file path/name. Files in the Backendless storage have the following URL structure:

https://api.backendless.com/<application id>/<REST API key>/files/<path>/<file name>

The API to delete a file uses the <path>/<filename> part to identify the file which must be deleted.

 

Synchronous method:

public void BackendlessAPI.Backendless.Files.Remove( string filePath )

Asynchronous method:

public void BackendlessAPI.Backendless.Files.Remove( string filePath, AsyncCallback callback )

where:

filePath - Path of the file to delete. The path must consist of the file path and file name.
callback - A responder object which receives a callback when the method successfully deletes the file or if an error occurs.

Example:

AsyncCallback<object> callback = new AsyncCallback<object>(
        result =>
        {
          System.Console.WriteLine( "File deleted" );
        },

        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessAPI.Backendless.Files.Remove( "myfiles/test.txt", callback );

 

Deleting a Directory

To delete a directory from the Backendless file storage, it must be identified by the its path. Directories in the Backendless storage have the following URL structure:

https://api.backendless.com/<application id>/<REST API Key>/files/<path>

The API to delete a directory uses the <path> element from the URL above.

 

Synchronous method:

public void BackendlessAPI.Backendless.Files.RemoveDirectory( string path )

Asynchronous method:

public void BackendlessAPI.Backendless.Files.RemoveDirectory( string path, AsyncCallback<object> callback )

where:

path - path of the directory to delete.
callback - a responder object which receives a callback when the method successfully deletes the directory or if an error occurs.

Example:

AsyncCallback<object> callback = new AsyncCallback<object>(
        result =>
        {
          System.Console.WriteLine( "Directory deleted" );
        },

        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessAPI.Backendless.Files.Remove( "myfiles/pics", callback );

Git Integration

Backendless file storage can also function as a git repository. This could be very convenient for deploying multiple files from the developer's computer with a single command. Git integration is disabled by default. To enable git for the file storage:

1. Open Backendless Console
2. Select your app/backend
3. Click Manage and scroll down to the Enable .git support section
4. Use the toggle to turn git integration on or off:
git-support

When the git integration is turned on, all files present in or uploaded to the file storage are immediately committed to the repository. This integration is bi-directional. It means that any files committed into the git repository by the means of git, will also be copied into the file storage. When git integration is being turned off, the git repository is deleted with all the associated history (the files remain in the file storage).

With the git integration enabled, a new folder (.git) appears in the File Browser on the Files screen. The folder contains the files from the git repository. When a file is uploaded to file storage either via the Upload API or using the File Browser, it is automatically committed to the repository. Likewise, when a file is pushed into the repository, it becomes available and visible in the file storage. The same applies to editing and deleting files either in the Backendless Console or in git repository.

When git is enabled, the repository is available at the following address:

https://gitv4.backendless.com/<application id>/.git

where:

<application id> - application ID available in Backendless Console at Manage > App Settings.

When the Backendless backend is configured with a custom domain name, the repository URL is:

http://<custom domain name>/.git

The repository uses the same authentication as Backendless Console. That means all git commands must use the same developer email address and password as for logging in to Backendless Console.

It is important to note that any system level files created by git are also placed into the file storage (the .git directory). These files are accounted for when calculating the file space used by the app/backend.

Configuring Local Environment

You have no local files and there is a remote GIT repository:

There are files in the Backendless storage and there are no files locally:

Clone existing repository:

mkdir /path/to/your/project
cd /path/to/your/project
git clone http://gitv4.backendless.com/<your application id>/.git
cd <your application id>

Adding a file locally and pushing to Backendless git:

> echo "First file" >> file.txt
> git add file.txt
> git commit -m 'Initial commit with new file'
> git push -u origin master

You have with an existing GIT project in your local environment:

This applies when you already have a local git project. You also enabled git integration in Backendless and need to "integrate" your local git project with the git repository in Backendless.

> cd /path/to/my/repo
> git remote add origin http://gitv4.backendless.com/<your application id>/.git
 
# pushes the repo and its refs for the first time to Backendless git
> git push -u origin --all
 
# pushes any tags to Backendless git
> git push -u origin --tags

You have an existing FILE project in your local environment.

This applies when you have existing files locally and need to add them to the git repository you initialized in Backendless.

> cd /path/to/my/repo
> git init
> git remote add origin http://gitv4.backendless.com/<your application id>/.git
> git pull -u origin master
> git add *
> git commit -m 'merge with existing project'
> git push -u origin master

Web Hosting

Backendless file storage includes a special directory which facilitates web hosting for the app/backend. The directory name is /web:

web-directory

The /web folder serves as the web server root. The web server is available at the following URLs:

With custom domain name enabled for the account:

http://custom domain name

Without custom domain name:

https://api.backendless.com/<application id>/<REST API key>/files/web

where:

<application id> - ID of the application which can be obtained from the Manage > App Settings screen of the Backendless Console
<REST API Key> - API Key assigned to the application by Backendless. It is available from the Manage > App Settings screen of the Backendless Console.

 

Custom Domain Name

Backendless File Service supports mapping of a custom domain name to the application's backend. As a result, once a domain name is mapped, the following backend's resources become available via the custom URL:

Service API endpoint. The default endpoint for all Backendless services is https://api.backendless.com. With a custom domain name, the endpoint is also available at: http://<custom domain name>/api
Web Hosting. Backendless file storage contains a special directory - /web, which serves as the web site root. When a custom domain name is mapped to a Backendless application/backend, the contents of the /web directory are served for the HTTP requests with the domain name. See the Web Hosting section for additional details.
git endpoint. When the Backendless git integration is enabled, the git endpoint with a custom domain name is:

http://<custom domain name>/.git

Before a custom domain name is assigned to a Backendless application:

1. Create a CNAME record in DNS for your custom domain name
2. Map the CNAME record to develop.backendless.com
3. Open Backendless Console and select your application/backend.
4. Click Manage and scroll down to the "Custom Domain" section.
5. Enter the domain name into the text field and click Save
custom-domain.zoom50

Custom Web Template Hosting

A client-to-backend workflow may include interaction with web pages presented to the users of the application. Consider the following scenarios:

User registration. When a user registers with an application, he receives an email with a link to a page. Clicking the link acknowledges that the email address is valid and the user account is confirmed.
Password change. When a user requests password change (or password recovery), an email is sent to the user with a link to a web page where they can reset the password.
Session expiration. When a user session with the application expires, he is redirected to a webpage.

All these use cases have something in common - they all force the user to interact with a web page. The templates for these pages are available in the /web/templates path of the backend's file storage:

files-web-templates-dirs

The look and feel as well as the logic in the pages can be customized by modifying the HTML/CSS /JS files provided for each template. For example, the contents of the change_password folder is:

change-password-template

The "out-of-the-box" rendering of the pages is shown below:

Registration confirmation page:

backendless-registration-conf

Password change page:

backendless-password-change

Session expiration page:

session-expiration

Files Security

Access to files and directories can be restricted using permissions. Backendless supports the following permissions for files and directories:

Read - permission to download a file. This permission can be applied to a directory, in that case it applies recursively to all files contained therein.
Write - permission to upload a file or modify a directory by uploading files into it.
Remove - permission to delete a file or a directory.

To modify the permission matrix for a file or a directory, click the "Permissions" icon in file browser in Backendless Console:

files-directory-permissions

 

The permission assignment popup contains two sections: USER PERMISSIONS and ROLES PERMISSIONS which allow permission management either for a specific user account or for application roles.  To modify a permission for an operation for a user or a role, click the icon in the corresponding column. The icon has 3 states:

ico-checkmark-gray - inherit GRANT permission from the global permission matrix. This is the default permission.

ico-checkmark-green - explicit GRANT of the permission for the operation. Allows the user to perform the operation.

icon-deny - DENY permission for the operation. Restricts the user from performing the operation.

files-role-permissions.zoom70

Permissions API

Backendless security mechanism assigns an Access Control List (ACL) to every file in the file storage. An ACL defines users and user roles that have permissions to read, write, or delete a file. The Permissions API allows programmatic control over file ACL by granting or denying permissions to a file for a user or a user role. Using the API permissions can be applies for a user or user role individually or in "bulk" - for all users or user roles in a single call.

The path or the url argument in the APIs below must identify a file or directory for which the permission is modified. The value must be the short version of the path. For example, suppose the full URL of a file in the file storage system is:

https://api.backendless.com/31CB9FED-F34C-5541-FF26-6C2B6719F200/23432-A6B2-FF6B-31CB9FED/files/movies/vacation.mp4

The path to the file in the API call must contain only the directory and the file name (without the leading slash):

movies/vacation.mp4

 

The user account (or the role) on which behalf the API is executed must contain the Permission permission. For example, if the call is made by an authenticated user, the role for the user account would be AuthenticatedUser. The role must have the Permission permission in order for the API call to go through.

 

Method Signatures

The following method signatures are used for granting or denying the access to file  or directory for a user, user role, all users, and all user roles

public void GrantForUser( String userId, String fileOrDirURL )
public void GrantForUser( String userId, String fileOrDirURL, AsyncCallback<Object> responder )
public void DenyForUser( String userId, String fileOrDirURL )
public void DenyForUser( String userId, String fileOrDirURL, AsyncCallback<Object> responder )
public void GrantForRole( String roleName, String fileOrDirURL )
public void GrantForRole( String roleName, String fileOrDirURL, AsyncCallback<Object> responder )
public void DenyForRole( String roleName, String fileOrDirURL )
public void DenyForRole( String roleName, String fileOrDirURL, AsyncCallback<Object> responder )
public void GrantForAllUsers( String fileOrDirURL )
public void GrantForAllUsers( String fileOrDirURL, AsyncCallback<Object> responder )
public void DenyForAllUsers( String fileOrDirURL )
public void DenyForAllUsers( String fileOrDirURL, AsyncCallback<Object> responder )
public void GrantForAllRoles( String fileOrDirURL )
public void GrantForAllRoles( String fileOrDirURL, AsyncCallback<Object> responder )
public void DenyForAllRoles( String fileOrDirURL )
public void DenyForAllRoles( String fileOrDirURL, AsyncCallback<Object> responder )

Use these methods to retrieve, upload, and remove a file

FilePermission.READ.<method> to set permission to load/retrieve a file
FilePermission.WRITE.<method> to set permission to upload a file
FilePermission.DELETE.<method> to set permission to remove a file
FilePermission.PERMISSION.<method> to change permissions for a file or directory

To grant access for a user

FilePermission.READ.GrantForUser( userId, fileOrDirURL );
FilePermission.WRITE.GrantForUser( userId, fileOrDirURL );
FilePermission.DELETE.GrantForUser( userId, fileOrDirURL );

where:

userId - ID of a user, for which you want to grant the read, write, or delete  permission.
fileOrDirURL - path to a file or directory, for which you want to specify the permission.

To grant access for a user role

FilePermission.READ.GrantForRole( roleName, fileOrDirURL );
FilePermission.WRITE.GrantForRole( roleName, fileOrDirURL );
FilePermission.DELETE.GrantForRole( roleName, fileOrDirURL );

where:

roleName - name of the user role, for which you want to grant the read, write, or delete  permission.
fileOrDirURL - path to a file or directory, for which you want to specify the permission.

To grant access for all users

FilePermission.READ.GrantForAllUsers( fileOrDirURL );
FilePermission.WRITE.GrantForAllUsers( fileOrDirURL );
FilePermission.DELETE.GrantForAllUsers( fileOrDirURL );

where:

fileOrDirURL - path to a file or directory, for which you want to specify the permission.

To grant access for all user roles

FilePermission.READ.GrantForAllRoles( fileOrDirURL );
FilePermission.WRITE.GrantForAllRoles( fileOrDirURL );
FilePermission.DELETE.GrantForAllRoles( fileOrDirURL );

where:

fileOrDirURL - path to a file or directory, for which you want to specify the permission.

 

To deny access for a user

FilePermission.READ.DenyForUser( userId, fileOrDirURL );
FilePermission.WRITE.DenyForUser( userId, fileOrDirURL );
FilePermission.DELETE.DenyForUser( userId, fileOrDirURL );

where:

userId - ID of a user, for which you want to deny the read, write, or delete  permission.
fileOrDirURL - path to a file or directory, for which you want to specify the permission.

To deny access for a user role

FilePermission.READ.DenyForRole( roleName, fileOrDirURL );
FilePermission.WRITE.DenyForRole( roleName, fileOrDirURL );
FilePermission.DELETE.DenyForRole( roleName, fileOrDirURL );

where:

roleName - name of the user role, for which you want to deny the read, write, or delete  permission.
fileOrDirURL - path to a file or directory, for which you want to specify the permission.

To deny access for all users

FilePermission.READ.DenyForAllUsers( fileOrDirURL );
FilePermission.WRITE.DenyForAllUsers( fileOrDirURL );
FilePermission.DELETE.DenyForAllUsers( fileOrDirURL );

where:

fileOrDirURL - path to a file or directory, for which you want to specify the permission.

To deny access for all user roles

FilePermission.READ.DenyForAllRoles( fileOrDirURL );
FilePermission.WRITE.DenyForAllRoles( fileOrDirURL );
FilePermission.DELETE.DenyForAllRoles( fileOrDirURL );

where:

fileOrDirURL - path to a file or directory, for which you want to specify the permission.

 

Geolocation API

Overview

Backendless Geolocation Service is a system supporting management and search of geo points. A geo point in the most primitive format consists of a pair of coordinates: latitude and longitude. Optionally a geo point may contain metadata, which is a collection of arbitrary key/value pairs. A geo point belongs to a category, which is a logical grouping of geo points. The diagram bellow illustrates these concepts:

backendless-geo-point-concepts.jpeg

 

Backendless allows infinite number of geo points managed for an application. Geo points can be added via an API call or the import functionality in Backendless console. Once the backend is populated with geo points, the search API can be used to run the following types of geo queries:

Radius-based search - Searches for geo points in a circular map area defined by the coordinates of the central point and a radius. Backendless returns all geo points within the area.
backendless-geo-radius-search
Search in a rectangular map area - Searches for geo points in a rectangular map area identified by the coordinates of two corners defining the area (North West and South East):
backendless-geo-rectangle-search

 

Additionally, the geo search API supports the following search options available in the APIs:

Filtering by categories - Both types of search (radius-based and rectangular) can specify the categories in which the backend should search for the geo points.
Relative search - Runs a search for a subset of metadata key/value pairs to match up to the specified threshold value. The threshold must be expressed as a percentage of matches.
Query-based search - The metadata associated with the geo points can be used in queries which should be formatted using the SQL-92 syntax. For example, the geo point shown in the image above can be discovered with the following queries:

cuisine = 'French'

cuisine LIKE 'Fr%' and Atmosphere = 'Casual'

cuisine = 'French' and (Price = '$$$$' or Price = '$$$')

Adding a GeoPoint

This API adds a geo point to the backend geo location storage. Once a geo point is added, it becomes searchable through all geopoint search mechanisms supported by Backendless (search in radius, search in a rectangular area, search in a category). At the present moment there are two ways to add geo points: (1) using the API documented below or (2) using the Backendless console's import function.

 

 

Synchronous method:

public GeoPoint SavePoint( GeoPoint geoPoint )

public GeoPoint SavePoint( double latitude, 
                           double longitude, 
                           Dictionary<string, string> metadata )

public GeoPoint SavePoint( double latitude, 
                           double longitude, 
                           List<string> categoryNames,
                           Dictionary<string, string> metadata )

     

Asynchronous method:

public void SavePoint( GeoPoint geoPoint, 
                       AsyncCallback<GeoPoint> callback )

public void SavePoint( double latitude, 
                       double longitude, 
                       Dictionary<string, string> metadata, 
                       AsyncCallback<GeoPoint> callback)

public void SavePoint( double latitude, 
                       double longitude, 
                       List<string> categoryNames,
                       Dictionary<string, string> metadata, 
                       AsyncCallback<GeoPoint> callback)

public void SavePoint( double latitude, 
                       double longitude, 
                       Dictionary<string, object> metadata, 
                       AsyncCallback<GeoPoint> callback)

public void SavePoint( double latitude, 
                       double longitude, 
                       List<string> categoryNames,
                       Dictionary<string, object> metadata, 
                       AsyncCallback<GeoPoint> callback)

where:

geoPoint - an instance of GeoPoint defining the properties of a geo point to add. See the class definition in the "Return Value" section.
latitude - latitude of the point to add.
longitude - longitude of the point to add.
categories - list of categories the point is added to. If a category does not exist at the time when a point is added, Backendless creates the category and adds the point to it. For the methods without this argument, the point is added to the "Default" category.
metadata - metadata associated with the geo point. Accepted values for this parameter are: String, Number (integer and double), and Data Service objects. Date values must be represented as number in the Unix timestamp format (number of milliseconds since January 1, 1970 at UTC). Learn more about using date in search queries for category, radius, or rectangular area search.
callback - a responder object which receives a callback when the method successfully retrieves a list of application's geo categories.

Return Value:

An instance of the GeoPoint class representing the new geo point added to the application's geo location storage. Below is a brief version of the class. The complete source code listing is available in the Backendless github repository.

Example:

AsyncCallback<BackendlessAPI.Geo.GeoPoint> callback = new AsyncCallback<BackendlessAPI.Geo.GeoPoint>(
  result =>
  {
    System.Console.WriteLine( "Geo point saved - " + result.ObjectId );
  },

  fault =>
  {
    System.Console.WriteLine( "Error - " + fault );
  } );

List<String> categories = new List<string>();
categories.Add( "restaurants" );
categories.Add( "cool_places" );

Dictionary<String, String> metadata = new Dictionary<string, object>();
metadata.Add( "name", "Eatzi's" );
GeoPoint geoPoint = new GeoPoint( 32.81, -96.80, categories, metadata );

BackendlessAPI.Backendless.Geo.SavePoint( geoPoint, callback );

 

Updating a GeoPoint

Geo update API relies on the same methods used for Adding a Geo Point. The primary difference is in order to update a geo point it must have the objectId property assigned by Backendless (when the geopoint is created or imported). The semantics of the properties in an update request is as follows:

objectId is a required property.
All other properties (latitude, longitude, categories, metadata) are optional, but at least one must contain a value.
If latitude or longitude contain values, the new values replace the existing ones.
If categories contains a value, the geo point is moved to the specified categories (with coordinates and metadata).
If categories is null, the geo point stays in the current category.
If metadata is null, the geo point keeps the current metadata.
If metadata contains any key/value pairs, the new metadata replaces the existing one.
If metadata is an empty object/dictionary, the existing metadata is removed.

Deleting a GeoPoint

There are two ways to delete a geopoint from the Geolocation storage:

 

Deleting a GeoPoint using the Backendless Console

To delete a geo point using the Backendless Console:

1. Log in to the Backendless Console, select your app and click the Geolocation icon.
2. Select a geo category containing the geopoint to be deleted.
3. Click the checkboxes next to the geopoint(s) which should be deleted.
4. Click Delete Selected from the button bar as shown below:
geo-delete-geopoints.zoom50
5. Click Delete in the confirmation popup to confirm the deletion.
6. A confirmation notification will appear in the top right corner.

 

Deleting a GeoPoint with the API

 

Synchronous method:

public void RemovePoint( GeoPoint geoPoint )

Asynchronous method:

public void RemovePoint( GeoPoint geoPoint, AsyncCallback<GeoPoint> callback )

where:

geoPoint - an instance of GeoPoint to delete from the geolocation storage.
callback - a responder object which receives a callback when the method successfully deletes the geopoint, or an error returned by the server.

Example:

The example below demonstrates how to delete a geopoint. A geopoint is added first, then subsequently deleted.

Synchronous API sample:

GeoPoint geoPoint = new GeoPoint( -31.96, 115.84 );
GeoPoint savedGeoPoint = Backendless.Geo.SavePoint( geoPoint );
Backendless.Geo.RemovePoint( savedGeoPoint );

Asynchronous API sample:

AsyncCallback<GeoPoint> removeCallback = new AsyncCallback<GeoPoint>(
  r =>
  {
    System.Console.WriteLine( "Geo point has been removed." );
  },
  error =>
  {
    System.Console.WriteLine( "Server reported an error - " + error.Message );
  } );

AsyncCallback<GeoPoint> saveCallback = new AsyncCallback<GeoPoint>(
  savedGeoPoint =>
  {
    System.Console.WriteLine( "Geo point has been saved. Object ID - " + savedGeoPoint.ObjectId );
    Backendless.Geo.RemovePoint( savedGeoPoint, removeCallback );
  },
  error =>
  {
  } );

GeoPoint geoPoint = new GeoPoint( -31.96, 115.84 );
Backendless.Geo.SavePoint( geoPoint, saveCallback );

Adding a Geo Category

This API creates a geo category. A geo category is a logical grouping of geo points. Category name may contain the following literals: a-z, A-Z, numbers 0-9 and the underscore (_ ) character. The name must start with a literal. Category names can be inspected using Backendless Console (see the image below) or using the API call retrieving a list of categories.

 

Adding Categories in Console

Backendless Console supports adding a category via the graphical interface. To create a category:

1. Login to Backendless Console and select your app/backend.
2. Click the Geolocation icon in the menu on the left.
3. Use the "plus" icon located above the section containing the list of categories:
create-new-geo-cat
4. Enter the category name in the popup and click "Save".

 
Adding Categories with the API
 

Synchronous method:

public GeoCategory BackendlessAPI.Backendless.Geo.AddCategory( string categoryName )

Asynchronous method:

public void BackendlessAPI.Backendless.Geo.AddCategory( string categoryName, AsyncCallback<GeoCategory> callback )

where:

categoryName - name of the category to create.
callback - a responder object which receives a callback when the method successfully creates the category or if an error occurs.

Return Value:

An instance of GeoCategory representing the new category with object properties for category's objectId, name and number of geo points in it:

namespace BackendlessAPI.Geo
{
    public class GeoCategory
    {
      public string Id { get; set; }

      public string Name { get; set; }

      public int Size { get; set; }
    }
}

 

Example:

AsyncCallback
     
       callback = new AsyncCallback<BackendlessAPI.Geo.GeoCategory>(
        result =>
        {
          System.Console.WriteLine( "Category created - " + result.Name );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessAPI.Backendless.Geo.AddCategory( "my_category", callback );

     

 

Deleting a Geo Category

This API deletes a geo category. If the category does not exist, the service returns an error.
 

Removing Categories in Console

Backendless Console supports category deletion via the graphical interface. To delete a category:

1. Login to Backendless Console
2. Select your app/backend.
3. Click the Geolocation icon in the menu on the left.
4. Use the "delete" icon in the section above the list of categories:
delete-geo-category
5. Select DELETE in the confirmation popup.

 
Deleting Categories with the API
 

Synchronous method:

public bool BackendlessAPI.Backendless.Geo.DeleteCategory( string categoryName )

Asynchronous method:

public void BackendlessAPI.Backendless.Geo.DeleteCategory( string categoryName, AsyncCallback<bool> callback )

where:

categoryName - name of the category to delete.
callback - a responder object which receives a callback when the method successfully deletes the category or if an error occurs.

Return Value:

true if the category is deleted, false otherwise.

Example:

AsyncCallback<bool> callback = new AsyncCallback<bool>(
        result =>
        {
          System.Console.WriteLine( "Category deleted - " + result );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessAPI.Backendless.Geo.DeleteCategory( "my_category", callback );

 

Retrieving Geo Categories

This API retrieves a list of all the application's geo categories.

 

Synchronous method:

public List<GeoCategory> BackendlessAPI.Backendless.Geo.GetCategories()

Asynchronous method:

public public void GetCategories( AsyncCallback<List<GeoCategory>> callback )

where:

callback - a responder object which receives a callback when the method successfully retrieves a list of application's geo categories.

Return Value:

A generic collection (List) of GeoCategory objects:

namespace BackendlessAPI.Geo
{
    public class GeoCategory
    {
      public string Id { get; set; }

      public string Name { get; set; }

      public int Size { get; set; }
    }
}

 

Example:

AsyncCallback<List<BackendlessAPI.Geo.GeoCategory>> callback = new AsyncCallback<List<BackendlessAPI.Geo.GeoCategory>>(
        result =>
        {
          System.Console.WriteLine( "Categories received - " + result.Count );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessAPI.Backendless.Geo.GetCategories( callback );

 

Get Geopoint Count

// TODO

Importing Geo Data

Backendless console supports bulk import of geo points with metadata. The import procedure automatically places the geo points into the specified categories. The raw data must be in a comma separated values (CSV) format. For more details about geopoint import see the Importing for Geo Service section of the guide.

Search in Category

This API supports two types of geo searches:

Search in one or more geo categories.
Search based on metadata properties in one or more categories .

 

Methods:

Synchronous method:



public BackendlessCollection<GeoPoint> BackendlessAPI.Backendless.Geo.GetPoints(BackendlessGeoQuery geoQuery)

Asynchronous method:



public void BackendlessAPI.Backendless.Geo.GetPoints(BackendlessGeoQuery geoQuery, AsyncCallback<BackendlessCollection<GeoPoint>> callback)

where:

geoQuery - a query object encapsulating the search parameters. See the Running Search Queries section below for details.
callback - a responder object which receives a callback when the search operation successfully completes (regardless if any points were found) or if an error occurs.

Return Value:

A collection of GeoPoint objects matching the search query. The class properties are:

ObjectId - an ID assigned to geo point by Backendless when it is saved in the backend geo location storage.
Latitude - latitude of the geo point.
Longitude - longitude of the geo point.
Categories - an array of geo categories the point belong to.
Metadata - metadata associated with the geo point. Accepted values for this parameter are: String, Number (integer and double), and Data Service objects. Date values must be represented as number in the Unix timestamp format (number of milliseconds since January 1, 1970 at UTC). Learn more about using date in search queries for category, radius, or rectangular area search.

 

Geo points returned from the search query are contained inside of BackendlessCollection. Since the search query may produce a large number of geo points, not all of them are returned at once. Instead, all found geo points are divided into 'pages'. The size of each page is determined by the pageSize parameter in the query object. The first response returns the first page. The collection class includes methods for loading additional pages. BackendlessCollection also includes the total number of all geo points found by the search operation (the TotalObjects value).

All geo points in the entire search result are indexed. The index of the first geo point is 0. The offset parameter in the query object and in some method of BackendlessCollection specifies the index from which to load the next page of geo points. For example, suppose the entire search result is 200 points (the TotalObjects value returned in the collection is 200). If the initial PageSize is 20, then only 20 geo points are returned in the first response. To get the second page of geo points, they should be loaded from offset 20, third from 40 and so on. The formula for calculating the offset is:
 [value of offset in the current response] + [size of current page ].

 

Below is a compressed version of the BackendlessCollection class.

 

Running Search Queries

The geo query object includes multiple parameters. Depending on which parameters contain values, the semantics of the search would change. Any search must be performed within at least one category. If no category names are provided, the search is performed in the Default category.

 

Search in categories

To search in one or more categories without any constraints on metadata or proximity to a center point, simply set the names of the categories in the query object. The request returns all geo points divided into pages of data, one page at a time.

AsyncCallback<BackendlessCollection<GeoPoint>> callback = new AsyncCallback<BackendlessCollection<GeoPoint>>(
        result =>
        {
          System.Console.WriteLine( "Found geo points - " + result.Data.Count );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.Categories.Add( "Restaurants" );
Backendless.Geo.GetPoints( geoQuery, callback );

Search in categories with metadata

Metadata-based search finds all geo points which match all specified metadata properties in the given categories. The example below searches for the geo points in the Restaurants category with metadata containing "Cuisine = French" and "Atmosphere = Romantic".

AsyncCallback<BackendlessCollection<GeoPoint>> callback = new AsyncCallback<BackendlessCollection<GeoPoint>>(
        result =>
        {
          System.Console.WriteLine( "Found geo points - " + result.Data.Count );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.Categories.Add( "Restaurants" );
geoQuery.Metadata.Add( "Cuisine", "French" );
geoQuery.Metadata.Add( "Atmosphere", "Romantic" );

Backendless.Geo.GetPoints( geoQuery, callback );

 
 

Using dates in where clause when searching in categories

The search query used to retrieve geo points may reference date values. These values must be stored as a number of milliseconds since January 1st, 1970 at UTC. The example below demonstrates the usage of a date/time timestamp in a search query:
 

AsyncCallback<BackendlessCollection<GeoPoint>> callback = new AsyncCallback<BackendlessCollection<GeoPoint>>(
        result =>
        {
          System.Console.WriteLine( "Found geo points - " + result.Data.Count );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.Categories.Add( "Restaurants" );
geoQuery.Metadata.Add( "Cuisine", "French" );
geoQuery.Metadata.Add( "Atmosphere", "Romantic" );

Backendless.Geo.GetPoints( geoQuery, callback );

 

Search in category by date by using synchronous call:

static void SearchByDateInCategorySync()
{
  // date
  DateTime updated = DateTime.Parse( "2015-01-17 12:00:00Z" );
 
  // create
  GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
  geoPoint.Categories.Add( "Coffee" );
  geoPoint.Metadata.Add( "Name", "Starbucks" );
  geoPoint.Metadata.Add( "Parking", true );
  geoPoint.Metadata.Add( "updated", DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond );

  geoPoint = Backendless.Geo.SavePoint( geoPoint );
  System.Console.WriteLine( String.Format( "SearchByDateInCategory -> point: {0}", geoPoint ) );

  // search
  BackendlessGeoQuery query = new BackendlessGeoQuery();
  query.Categories.Add( "Coffee" );
  query.WhereClause = String.Format( "updated > {0}", updated.Ticks / TimeSpan.TicksPerMillisecond );
  query.IncludeMeta = true;
  BackendlessCollection<GeoPoint> points = Backendless.Geo.GetPoints( query );

  System.Console.WriteLine( String.Format( "SearchByDateInCategory GETPOINTS: {0}", String.Join( ",", points.GetCurrentPage() ) ) );
}

Search in category by date by using asynchronous call:

static void SearchByDateInCategoryAsync()
{
  // date
  DateTime updated = DateTime.Parse( "2015-01-17 12:00:00Z" );

  AsyncCallback<BackendlessCollection<GeoPoint>> searchCallback = new AsyncCallback<BackendlessCollection<GeoPoint>>(
    result =>
    {
      System.Console.WriteLine( String.Format( "\nSearchByDateInCategory GETPOINTS: {0}", String.Join( ",", result.GetCurrentPage() ) ) );
    },
    fault =>
    {
      System.Console.WriteLine( "Error - " + fault );
    } );

  AsyncCallback<GeoPoint> saveGeoPointCallback = new AsyncCallback<GeoPoint>(
    result =>
    {
      System.Console.WriteLine( String.Format( "SearchByDateInCategory -> point: {0}", result ) );

      // search
      BackendlessGeoQuery query = new BackendlessGeoQuery();
      query.Categories.Add( "Coffee" );
      query.WhereClause = String.Format( "updated > {0}", updated.Ticks / TimeSpan.TicksPerMillisecond );
      query.IncludeMeta = true;
      Backendless.Geo.GetPoints( query, searchCallback );
    },
    fault =>
    {
      System.Console.WriteLine( "Error - " + fault );
    } );

  // create
  GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
  geoPoint.Categories.Add( "Coffee" );
  geoPoint.Metadata.Add( "Name", "Starbucks" );
  geoPoint.Metadata.Add( "Parking", true );
  geoPoint.Metadata.Add( "updated", DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond );

  Backendless.Geo.SavePoint( geoPoint, saveGeoPointCallback );
}

Requesting meta in response

Geo points returned in the search results do not include their metadata properties by default. The search query object includes a property which can be used to request the metadata to be included. This property can be used with any search options described above. For example, the following code runs a search in a category and requests the metadata to be included:

AsyncCallback<BackendlessCollection<GeoPoint>> callback = new AsyncCallback<BackendlessCollection<GeoPoint>>(
        result =>
        {
          System.Console.WriteLine( "Found geo points - " + result.Data.Count );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.Categories.Add( "Restaurants" );
geoQuery.IncludeMeta = true;
Backendless.Geo.GetPoints( geoQuery, callback );

 

Search in Radius

This API supports multiple types of geo searches:

Search for geo points located within specified distance (radius) from a given point.
Search in radius based on metadata.

 

Synchronous method:

public BackendlessCollection<GeoPoint> BackendlessAPI.Backendless.Geo.GetPoints(BackendlessGeoQuery geoQuery)

Asynchronous method:

public void BackendlessAPI.Backendless.Geo.GetPoints(BackendlessGeoQuery geoQuery, AsyncCallback<BackendlessCollection<GeoPoint>> callback)

where:

geoQuery - a query object encapsulating the search parameters. See the Running Search Queries section below for details.
callback - a responder object which receives a callback when the search operation successfully completes (regardless if any points were found) or if an error occurs.

 

Return Value:

A collection of GeoPoint objects matching the search query. Below is a brief version of the GeoPoint class. The complete source code listing is available in the Backendless github repository.
 

hmtoggle_plus1        GeoPoint.cs

using System.Collections.Generic;

namespace BackendlessAPI.Geo
{
  public class GeoPoint
  {
    private List<string> _categories;
    private Dictionary<string, string> _metadata;
    public string ObjectId { get; set; }
    public double Latitude { get; set; }
    public double Longitude { get; set; }
    public double Distance { get; set; }

    public GeoPoint()
    {
    }

    public GeoPoint( double latitude, double longitude )
    {
      Latitude = latitude;
      Longitude = longitude;
    }

    public GeoPoint( double latitude, double longitude, List<string> categories, Dictionary<string, string> metadata )
    {
      Latitude = latitude;
      Longitude = longitude;
      Categories = categories;
      Metadata = metadata;
    }

    public List<string> Categories
    {
      get { return _categories ?? (_categories = new List<string>()); }
      set { _categories = value; }
    }

    public Dictionary<string, string> Metadata
    {
      get { return _metadata ?? (_metadata = new Dictionary<string, string>()); }
      set { _metadata = value; }
    }
  }
}

where the geo point object properties are:

ObjectId - ID assigned to geo point by Backendless when it is saved in the backend geo location storage.
Latitude - latitude of the geo point.
Longitude - longitude of the geo point.
Categories - an array of geo categories the point belong to.
Metadata - metadata associated with the geo point. Accepted values for this parameter are: String, Number (integer and double), and Data Service objects. Date values must be represented as number in the Unix timestamp format (number of milliseconds since January 1, 1970 at UTC). Learn more about using date in search queries for category, radius, or rectangular area search.
Distance - distance between the center point referenced in the search query and the geo point. The distance is always in units specified in the search query.

 

Geo points returned from the search query are contained inside of BackendlessCollection. Since the search query may produce a large number of geo points, not all of them are returned at once. Instead, all found geo points are divided into 'pages'. The size of each page is determined by the pageSize parameter in the query object. The first response returns the first page. The collection class includes methods for loading additional pages. BackendlessCollection also includes the total number of all geo points found by the search operation (the TotalObjects value).

All geo points in the entire search result are indexed. The index of the first geo point is 0. The offset parameter in the query object and in some method of BackendlessCollection specifies the index from which to load the next page of geo points. For example, suppose the entire search result is 200 points (the TotalObjects value returned in the collection is 200). If the initial PageSize is 20, then only 20 geo points are returned in the first response. To get the second page of geo points, they should be loaded from offset 20, third from 40 and so on. The formula for calculating the offset is:
 [value of offset in the current response] + [size of current page ].

 

The geo points in the search results will be sorted by their proximity to the central point (center of the radius): the geo points that are closest to the central point will be listed first.

 

Below is a compressed version of the BackendlessCollection class. The full source code listing is available in the Backendless github repository:
 

hmtoggle_plus1        BackendlessCollection.cs

using System;
using System.Collections.Generic;
using System.Threading;
using BackendlessAPI.Async;
using BackendlessAPI.Exception;
using BackendlessAPI.Geo;
using BackendlessAPI.Persistence;

namespace BackendlessAPI.Data
{
  public class BackendlessCollection<T>
  {
    public int TotalObjects { get; set; }
    public List<T> Data { get; set; }
    public IBackendlessQuery Query { get; set; }
    public int PageSize
    {
      get { return Query.PageSize == null ? 0 : (int) Query.PageSize; }
      set { Query.PageSize = value; }
    }

    public List<T> GetCurrentPage()
    {
      return Data;
    }

    //Sync methods
    public BackendlessCollection<T> NextPage()
    public BackendlessCollection<T> PreviousPage()
    public BackendlessCollection<T> GetPage( int pageSize, int offset )

    //Async methods
    public void NextPage( AsyncCallback<BackendlessCollection<T>> responder )
    public void PreviousPage( AsyncCallback<BackendlessCollection<T>> responder )
    public void GetPage( int pageSize, int offset, AsyncCallback<BackendlessCollection<T>> responder )
}

 

Running Search Queries

The geo query object includes multiple parameters, none of them are required. As a result, depending on which parameters contain values, the semantics of the search would change. Any search must be performed within at least one category. If no category names are provided, the search is performed in the Default category.

 

Search in categories with radius

Radius-based search establishes a circular area by setting the coordinates of a central point and a distance (radius). Backendless searches for geo points in the specified distance from the coordinates in the center and includes them into the search result. The value of the distance is interpreted based in the units parameter, which can be METERS, KILOMETERS, MILES, YARDS, FEET:

AsyncCallback<BackendlessCollection<GeoPoint>> callback = new AsyncCallback<BackendlessCollection<GeoPoint>>(
        result =>
        {
          System.Console.WriteLine( "Found geo points - " + result.Data.Count );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.Categories.Add( "Restaurants" );
geoQuery.Latitude = 41.48;
geoQuery.Longitude = 2.15;
geoQuery.Radius = 100000;
geoQuery.Units = BackendlessAPI.Geo.Units.METERS;

Backendless.Geo.GetPoints( geoQuery, callback );

Search in categories with radius and metadata

This is the same as above, with the difference that the search result includes only geo points with the matching metadata:

AsyncCallback<BackendlessCollection<GeoPoint>> callback = new AsyncCallback<BackendlessCollection<GeoPoint>>(
        result =>
        {
          System.Console.WriteLine( "Found geo points - " + result.Data.Count );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.Categories.Add( "Restaurants" );
geoQuery.Latitude = 41.48;
geoQuery.Longitude = 2.15;
geoQuery.Radius = 100000;
geoQuery.Units = BackendlessAPI.Geo.Units.METERS;

geoQuery.Metadata.Add( "Cuisine", "French" );
geoQuery.Metadata.Add( "Atmosphere", "Romantic" );

Backendless.Geo.GetPoints( geoQuery, callback );

 

Using dates in where clause when searching in radius

The search query used to retrieve geo points may reference date values. These values must be stored as a number of milliseconds since January 1st, 1970 at UTC. The example below demonstrates the usage of a date/time timestamp in a search condition:

 

 

Search in radius by date by using synchronous call:

static void SearchByDateInRadiusSync()
{
  // date
  DateTime updated = DateTime.Parse( "2015-01-17 12:00:00Z" );

  // create
  GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
  geoPoint.Categories.Add( "Coffee" );
  geoPoint.Categories.Add( "City" );
  geoPoint.Metadata.Add( "Name", "Starbucks" );
  geoPoint.Metadata.Add( "Parking", true );
  geoPoint.Metadata.Add( "updated", DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond );

  geoPoint = Backendless.Geo.SavePoint( geoPoint );
  System.Console.WriteLine( String.Format( "SearchByDateInRadius -> point: {0}", geoPoint ) );

  // search
  BackendlessGeoQuery query = new BackendlessGeoQuery( 21.30, -157.85, 50, Units.KILOMETERS );
  query.Categories.Add( "City" );
  query.WhereClause = String.Format( "updated > {0}", updated.Ticks / TimeSpan.TicksPerMillisecond );
  query.IncludeMeta = true;
  BackendlessCollection<GeoPoint> points = Backendless.Geo.GetPoints( query );

  System.Console.WriteLine( String.Format( "SearchByDateInRadius GETPOINTS: {0}", String.Join( ",", points.GetCurrentPage() ) ) );
}

Search in radius by date by using asynchronous call:

static void SearchByDateInRadiusAsync()
{
  // date
  DateTime updated = DateTime.Parse( "2015-01-17 12:00:00Z" );

  AsyncCallback<BackendlessCollection<GeoPoint>> searchCallback = new AsyncCallback<BackendlessCollection<GeoPoint>>(
    result =>
    {
      System.Console.WriteLine( String.Format( "\nSearchByDateInRadius GETPOINTS: {0}", String.Join( ",", result.GetCurrentPage() ) ) );
    },
    fault =>
    {
      System.Console.WriteLine( "Error - " + fault );
    } );

  AsyncCallback<GeoPoint> saveGeoPointCallback = new AsyncCallback<GeoPoint>(
    result =>
    {
      System.Console.WriteLine( String.Format( "SearchByDateInRadius -> point: {0}", result ) );

      // search
      BackendlessGeoQuery query = new BackendlessGeoQuery( 21.30, -157.85, 50, Units.KILOMETERS );
      query.Categories.Add( "City" );
      query.WhereClause = String.Format( "updated > {0}", updated.Ticks / TimeSpan.TicksPerMillisecond );
      query.IncludeMeta = true;
      Backendless.Geo.GetPoints( query, searchCallback );
    },
    fault =>
    {
      System.Console.WriteLine( "Error - " + fault );
    } );

  // create
  GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
  geoPoint.Categories.Add( "Coffee" );
  geoPoint.Metadata.Add( "Name", "Starbucks" );
  geoPoint.Metadata.Add( "Parking", true );
  geoPoint.Metadata.Add( "updated", DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond );

  Backendless.Geo.SavePoint( geoPoint, saveGeoPointCallback );
}

Requesting meta in response

Geo points returned in the search results do not include their metadata properties by default. The search query object includes a property which can be used to request the metadata to be included. This property can be used with any search options described above. The syntax for requesting metadata in response is described in the Search in Category section.

Search in Rectangular Area

This API runs a search within a rectangular area of the map. The area is defined with the coordinates of the North West and South East corners of the map rectangle.

Synchronous method:

public BackendlessCollection<GeoPoint> BackendlessAPI.Backendless.Geo.GetPoints(BackendlessGeoQuery geoQuery)

Asynchronous method:

public void BackendlessAPI.Backendless.Geo.GetPoints(BackendlessGeoQuery geoQuery, AsyncCallback<BackendlessCollection<GeoPoint>> callback)

where:

geoQuery - a query object encapsulating the search parameters. See the Running Search Queries section below for details.
callback - a responder object which receives a callback when the search operation successfully completes (regardless if any points were found) or if an error occurs.

 

Return Value:

A collection of GeoPoint objects matching the search query. Below is a brief version of the GeoPoint class. The complete source code listing is available in the Backendless github repository.

hmtoggle_plus1        GeoPoint.cs

using System.Collections.Generic;

namespace BackendlessAPI.Geo
{
  public class GeoPoint
  {
    private List<string> _categories;
    private Dictionary<string, string> _metadata;
    public string ObjectId { get; set; }
    public double Latitude { get; set; }
    public double Longitude { get; set; }
    public double Distance { get; set; }

    public GeoPoint()
    {
    }

    public GeoPoint( double latitude, double longitude )
    {
      Latitude = latitude;
      Longitude = longitude;
    }

    public GeoPoint( double latitude, double longitude, List<string> categories, Dictionary<string, string> metadata )
    {
      Latitude = latitude;
      Longitude = longitude;
      Categories = categories;
      Metadata = metadata;
    }

    public List<string> Categories
    {
      get { return _categories ?? (_categories = new List<string>()); }
      set { _categories = value; }
    }

    public Dictionary<string, string> Metadata
    {
      get { return _metadata ?? (_metadata = new Dictionary<string, string>()); }
      set { _metadata = value; }
    }
  }
}

where the geo point object properties are:

ObjectId - ID assigned to geo point by Backendless when it is saved in the backend geo location storage.
Latitude - latitude of the geo point.
Longitude - longitude of the geo point.
Categories - an array of geo categories the point belong to.
Metadata - metadata associated with the geo point. Accepted values for this parameter are: String, Number (integer and double), and Data Service objects. Date values must be represented as number in the Unix timestamp format (number of milliseconds since January 1, 1970 at UTC). Learn more about using date in search queries for category, radius, or rectangular area search.
Distance - distance between the center point referenced in the search query and the geo point. The distance is always in units specified in the search query.

 

Geo points returned from the search query are contained inside of BackendlessCollection. Since the search query may produce a large number of geo points, not all of them are returned at once. Instead, all found geo points are divided into 'pages'. The size of each page is determined by the pageSize parameter in the query object. The first response returns the first page. The collection class includes methods for loading additional pages. BackendlessCollection also includes the total number of all geo points found by the search operation (the TotalObjects value).

All geo points in the entire search result are indexed. The index of the first geo point is 0. The offset parameter in the query object and in some method of BackendlessCollection specifies the index from which to load the next page of geo points. For example, suppose the entire search result is 200 points (the TotalObjects value returned in the collection is 200). If the initial PageSize is 20, then only 20 geo points are returned in the first response. To get the second page of geo points, they should be loaded from offset 20, third from 40 and so on. The formula for calculating the offset is:
 [value of offset in the current response] + [size of current page ].

 

Below is a compressed version of the BackendlessCollection class. The full source code listing is available in the Backendless github repository:

using System;
using System.Collections.Generic;
using System.Threading;
using BackendlessAPI.Async;
using BackendlessAPI.Exception;
using BackendlessAPI.Geo;
using BackendlessAPI.Persistence;

namespace BackendlessAPI.Data
{
  public class BackendlessCollection<T>
  {
    public int TotalObjects { get; set; }
    public List<T> Data { get; set; }
    public IBackendlessQuery Query { get; set; }
    public int PageSize
    {
      get { return Query.PageSize == null ? 0 : (int) Query.PageSize; }
      set { Query.PageSize = value; }
    }

    public List<T> GetCurrentPage()
    {
      return Data;
    }

    //Sync methods
    public BackendlessCollection<T> NextPage()
    public BackendlessCollection<T> PreviousPage()
    public BackendlessCollection<T> GetPage( int pageSize, int offset )

    //Async methods
    public void NextPage( AsyncCallback<BackendlessCollection<T>> responder )
    public void PreviousPage( AsyncCallback<BackendlessCollection<T>> responder )
    public void GetPage( int pageSize, int offset, AsyncCallback<BackendlessCollection<T>> responder )
}

Running Search Queries

The geo query object includes multiple parameters, however, only the coordinates defining the rectangular area are required.  A search query must be performed within at least one category. If no category names are provided, the search is performed in the Default category.

 

Search in a rectangle in categories

Rectangle-based search establishes a geographic area by setting the coordinates of the North West and South East corners of the area. Backendless searches for geo points in the specified area and includes them into the search result:
 

AsyncCallback<BackendlessCollection<GeoPoint>> callback = new AsyncCallback<BackendlessCollection<GeoPoint>>(
        result =>
        {
          System.Console.WriteLine( "Found geo points - " + result.Data.Count );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.Categories.Add( "Restaurants" );
geoQuery.SearchRectangle = new double[] {32.78, -96.8, 25.79, -80.22};

Backendless.Geo.GetPoints( geoQuery, callback );

Search in categories in a rectangular area and metadata

This is the same as above, with the difference that the search result includes only geo points with the matching metadata:

AsyncCallback<BackendlessCollection<GeoPoint>> callback = new AsyncCallback<BackendlessCollection<GeoPoint>>(
        result =>
        {
          System.Console.WriteLine( "Found geo points - " + result.Data.Count );
        },
        fault =>
        {
          System.Console.WriteLine( "Error - " + fault );
        } );

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.Categories.Add( "Restaurants" );
geoQuery.SearchRectangle = new double[] {32.78, -96.8, 25.79, -80.22};

geoQuery.Metadata.Add( "Cuisine", "French" );
geoQuery.Metadata.Add( "Atmosphere", "Romantic" );

Backendless.Geo.GetPoints( geoQuery, callback );

Using dates in where clause

The search query used to retrieve geo points may reference date values. These values must be stored as a number of milliseconds since January 1st, 1970 at UTC. The example below demonstrates the usage of a date/time timestamp in a search query:

 

Search in rectangular area by date by using synchronous call:

static void SearchByDateInRectangularAreaSync()
{
  // date
  DateTime openTime = DateTime.Parse( "2015-01-17 07:00:00" );
  DateTime closeTime = DateTime.Parse( "2015-01-17 23:00:00" );
  DateTime now = DateTime.Parse( "2015-01-17 15:20:00" );

  // create
  GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
  geoPoint.Categories.Add( "Coffee" );
  geoPoint.Metadata.Add( "Name", "Starbucks" );
  geoPoint.Metadata.Add( "openTime", openTime.Ticks / TimeSpan.TicksPerMillisecond );
  geoPoint.Metadata.Add( "closeTime", closeTime.Ticks / TimeSpan.TicksPerMillisecond );
  geoPoint = Backendless.Geo.SavePoint( geoPoint );
  System.Console.WriteLine( String.Format( "SearchByDateInRectangularAreaSync -> point: {0}", geoPoint ) );

  // search
  GeoPoint northWestCorner = new GeoPoint( 21.306944 + 0.5, -157.858333 - 0.5 );
  GeoPoint southEastCorner = new GeoPoint( 21.306944 - 0.5, -157.858333 + 0.5 );
  BackendlessGeoQuery query = new BackendlessGeoQuery( northWestCorner, southEastCorner );
  query.Categories.Add( "Coffee" );
  query.WhereClause = String.Format( "openTime < {0} AND closeTime > {0}", now.Ticks / TimeSpan.TicksPerMillisecond );
  query.IncludeMeta = true;
  BackendlessCollection<GeoPoint> points = Backendless.Geo.GetPoints( query );

  System.Console.WriteLine( String.Format( "SearchByDateInRectangularAreaSync GETPOINTS: {0}", String.Join( ",", points.GetCurrentPage() ) ) );
}

Search in rectangular area by date by using asynchronous call:

static void SearchByDateInRectangularAreaAsync()
{
  // date
  DateTime openTime = DateTime.Parse( "2015-01-17 07:00:00" );
  DateTime closeTime = DateTime.Parse( "2015-01-17 23:00:00" );
  DateTime now = DateTime.Parse( "2015-01-17 15:20:00" );

  AsyncCallback<BackendlessCollection<GeoPoint>> searchCallback = new AsyncCallback<BackendlessCollection<GeoPoint>>(
    result =>
    {
      System.Console.WriteLine( String.Format( "\nSearchByDateInRectangularAreaAsync GETPOINTS: {0}", String.Join( ",", result.GetCurrentPage() ) ) );
    },
    fault =>
    {
      System.Console.WriteLine( "Error - " + fault );
    } );

  AsyncCallback<GeoPoint> saveGeoPointCallback = new AsyncCallback<GeoPoint>(
    result =>
    {
      System.Console.WriteLine( String.Format( "SearchByDateInRectangularAreaAsync -> point: {0}", result ) );

      // search
      GeoPoint northWestCorner = new GeoPoint( 21.306944 + 0.5, -157.858333 - 0.5 );
      GeoPoint southEastCorner = new GeoPoint( 21.306944 - 0.5, -157.858333 + 0.5 );
      BackendlessGeoQuery query = new BackendlessGeoQuery( northWestCorner, southEastCorner );
      query.Categories.Add( "Coffee" );
      query.WhereClause = String.Format( "openTime < {0} AND closeTime > {0}", now.Ticks / TimeSpan.TicksPerMillisecond );
      query.IncludeMeta = true;
      Backendless.Geo.GetPoints( query, searchCallback );
    },
    fault =>
    {
      System.Console.WriteLine( "Error - " + fault );
    } );

  // create
  GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
  geoPoint.Categories.Add( "Coffee" );
  geoPoint.Metadata.Add( "Name", "Starbucks" );
  geoPoint.Metadata.Add( "openTime", openTime.Ticks / TimeSpan.TicksPerMillisecond );
  geoPoint.Metadata.Add( "closeTime", closeTime.Ticks / TimeSpan.TicksPerMillisecond );
  Backendless.Geo.SavePoint( geoPoint, saveGeoPointCallback );
}

Requesting meta in response

Geo points returned in the search results do not include their metadata properties by default. The search query object includes a property which can be used to request the metadata to be included. This property can be used with any search options described above. The syntax for requesting metadata in response is described in the Search in Category section.

GeoPoint Clustering

Geo point search in a category, radius or a rectangular area may return too many geo points within close geographic proximity from each other. This might be difficult to view and process in a client application. To address this problem, Backendless supports the geo clustering feature. A geo cluster is a group of several geo points located close to each other. The two screenshots below demonstrate the advantages of clustering: the picture on the top displays search results in the Backendless Console with clustering turned off and the one on the bottom displays search results as clusters when clustering has been enabled:

 

Geo Points View:

clustering-points.zoom50

 

Clusters and Points View:

clustering-clusters.zoom50

 

Backendless creates clusters by splitting the map into a grid of squares. Geo points which belong to a square are placed into the same cluster. When a square contains only one point, it remains non-clustered.

 

Testing Geo Clustering in Backendless Console

The Geolocation page displays non-clustered geo points by default. To see how geoclustering works and test geoclustering search results:

1. Log in to Backendless Console, select an application and click the Geolocation icon.
2. Click the Map-driven navigation toggle. The toggle changes how the geo points are loaded from the backend. In the map-driven mode console loads the geo points for the visible area of the map.
3. Click the Geo Clustering toggle to enable clustering.
4. Console reloads geo points and clusters for the current viewport of the map and displays the results. A cluster is visualized as a blue marker on the map with a number indicating how many geo points it represents.
geofence-screen7.zoom50

 

Geo clustering is also available with the "Search in Radius" option, which searches for geo points in a circular area. To enable this functionality, click the Search in radius toggle:

geofence-screen8.zoom50

 

If you want to see the geo points in a cluster, zoom in the map or double-click a cluster's marker. Zooming the map in too much (when using the clustering along with the search in radius) may result that the search radius will be much bigger than the visible part of the map on the screen. In this case, zoom out so you can adjust the circle position and/or radius.

 

Clicking a cluster's marker will reveal the coordinates and combined metadata of all geopoints in the cluster.

geofence-screen9.zoom50

Retrieving Clustered Geo Points

Geo point clustering can be requested by setting clustering parameters in BackendlessGeoQuery, which is used in the calls to retrieve geo points from a category, radius or rectangular area.

 

public void SetClusteringParams( double westLongitude, double eastLongitude, int mapWidth, int cluisterGridSize )

// same as above, but uses the default clusterGridSize of 100
public void SetClusteringParams( double westLongitude, double eastLongitude, int mapWidth )

where:

westLongitude - the longitude of any point on the western boundary of the map in degrees.
eastLongitude - the longitude of any point on the eastern boundary of the map in degrees.
mapWidth - the size of the viewing area of the map in pixels.
clusterGridSize - the size in pixels of the grid's squares used to group points into clusters. The default value is 100 pixels

Example:

Once the clustering parameters are set, the geo point search API will return clustered geo points. The return value is a collection of GeoCluster and/or GeoPoint objects. Instances of the latter may be returned when a geo point does not have any neighboring points within the grid's square it belongs to. The GeoCluster class extends from GeoPoint and supports all the inherited properties: latitude, longitude, categories and metadata. Additionally, geo cluster has its own property representing the number of points the cluster consists of. Since a GeoCluster object an instance of the GeoPoint class, the processing of search responses may look like this:

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.Radius = 1000;
geoQuery.Units = Units.MILES;
geoQuery.Latitude = 33.217;
geoQuery.Longitude = -97.134;
geoQuery.Categories.Add( "geoservice_sample" );
geoQuery.SetClusteringParams( -164.458, -29.809, 1532, 400 );
BackendlessCollection<GeoPoint> points = Backendless.Geo.GetPoints( geoQuery );

foreach( GeoPoint point in points.Data )
{
  System.Console.WriteLine( "latitude - " + point.Latitude );
  System.Console.WriteLine( "longitude - " + point.Longitude );

  if( point is GeoCluster )
  {
    GeoCluster geoCluster = (GeoCluster) point;
    System.Console.WriteLine( "Cluster. Number of points - " + geoCluster.TotalPoints );
  }

 System.Console.WriteLine( "-------------------------------" );
}

Loading Geo Points from a Cluster

With the geo clustering feature enabled, you may need to reveal the geo points gathered in a cluster. In the Backendless Console, you can click the cluster to reveal its details as described above. By using the calls described below, you will be able to reveal the geo points in a cluster via API.

Method:

public BackendlessCollection<GeoPoint> GetPoints( GeoCluster geoCluster )
public void GetPoints( GeoCluster geoCluster, AsyncCallback<BackendlessCollection<GeoPoint>> callback )

where:

 

geoCluster - a reference to a geo cluster for which to load the geo points.
callback - a responder object with references to methods handling successful result of invocation or an error.

Example:

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.Radius = 1000;
geoQuery.Units = Units.MILES;
geoQuery.Latitude = 33.217;
geoQuery.Longitude = -97.134;
geoQuery.Categories.Add( "geoservice_sample" );
geoQuery.SetClusteringParams( -164.458, -29.809, 1532, 400 );

AsyncCallback<BackendlessCollection<GeoPoint>> loadGeoPointsResponder;
loadGeoPointsResponder = new AsyncCallback<BackendlessCollection<GeoPoint>>(
  result => 
  {
        System.Console.WriteLine( "points in cluster:" );
        foreach( GeoPoint point in result.GetCurrentPage() )
        {
          System.Console.WriteLine( "Latitude - " + point.Latitude );
          System.Console.WriteLine( "Longitude - " + point.Longitude );
        }
        System.Console.WriteLine( "-------------------------" );
  },
  error => 
  {
    System.Console.WriteLine( String.Format( "Server reported an error - {0}" ), error.Message );
  });

AsyncCallback<BackendlessCollection<GeoPoint>> responder;
responder = new AsyncCallback<BackendlessCollection<GeoPoint>>(
  result =>
  {
    foreach( GeoPoint point in result.GetCurrentPage() )
    {
      if( point is GeoCluster )
      {
        GeoCluster geoCluster = (GeoCluster) point;
        System.Console.WriteLine( String.Format( "Cluster. Number of points - {0}", geoCluster.TotalPoints ) );

        Backendless.Geo.GetPoints( geoCluster, loadGeoPointsResponder );
      }
    }
  },
  error =>
  {
    System.Console.WriteLine( String.Format( "Server reported an error - {0}" ), error.Message );
  } );

Backendless.Geo.GetPoints( geoQuery, responder );

 

Relations with Data Objects

Backendless Data Service manages application's data objects and provides APIs to work with data objects. Backendless provides integration between data objects managed by Data Service and geo points managed by Geo Service for the scenarios when a logical connection between the two types must exist in an application. For instance, in a taxi ordering app a data object may represent a taxi car, while a geo point represents its location on the map. It is logical to link the two together so they can be retrieved and managed at once.

The Geo-to-Data integration is implemented through geo point metadata. A metadata property may reference one or more data objects. These relations may be created using the API or with Backendless Console. Once a relation is established, the console displays it in the Metadata column as a link to related data object(s). When a geo point is retrieved using the API, any related data objects can be retrieved by using the same principle for loading geo point metadata. The geo-to-data relation is bidirectoral, that is, a data object may reference a geo point through object properties (table columns). You can learn more about it in the Relations with Geo Points section of the Data documentation.

 

Apart from linking with the data objects, you can also link a geo point with a user object. Establishing relations with a user objects is performed the same way as with a data object.

 

Establishing Relations with a Data Object via Console

To link a geo point with a data object:

1. Click the Geolocation icon to open the Geo location screen.
2. Select a geo category to get a list of geo points from it.
3. Click the plus icon for a geo point you want to link with a data object.
geofence-screen10.zoom50

 

4. The Add Related Object pop-up window will display.
5. Type in a metadata property name in the Property field. This is the name of a new metadata property which will point to a related data object:
geofence-screen11.zoom70
6. Select a data table from from the Table drop-down menu. If you want to establish relation with a user object, select the Users  table from the drop-down menu. A list of the data objects which belong to the selected table will display.
7. Select the check-boxes for the data object(s) you want to link the geo point with.
8. Click the Add Related Objects button to establish a relation and save the changes.

 

Once a relation is established, the name of the property and the related data table name will display for the geo point:

geofence-screen12.zoom50

Updating/Deleting Relations

You can update or delete a geo to data relation by following the same process as above. The only difference is in order to access the"relation management" popup click the yellow box identifying the relation:

Create a Geo to Data Relation with the API

Creating a relationship between a geo point and data objects uses the same API as saving a geo point with metadata. The data object referenced in the metadata is saved in a table in the Data Service storage.

 

The example below adds a geo point representing a location of a taxi cab. The geo point includes the "TaxiCab" metadata property which references an object from the TaxiCab data table. This is an example of a one-to-one relation (one geo point is related to one data object).

First, declare the TaxiCab class:

class TaxiCab 
  { 
    public String CarMake; 
    public String CarModel; 
    public GeoPoint Location { get; set; } 
    public List<GeoPoint> PreviousDropOffs { get; set; } 
  }

Create a one-to-one relation:

TaxiCab cab = new TaxiCab();
cab.CarMake = "Ford";
cab.CarModel = "Crown Victoria";
GeoPoint pickupLocation = new GeoPoint( 40.750549, -73.994232 );
pickupLocation.Categories.Add( "Pickups" );
pickupLocation.Metadata.Add( "TaxiCab", cab );
Backendless.Geo.AddPoint( pickupLocation );

Alternatively create a one-to-many relation:

TaxiCab cab1 = new TaxiCab();
cab1.CarMake = "Ford";
cab1.CarModel = "Crown Victoria";

TaxiCab cab2 = new TaxiCab();
cab2.CarMake = "Toyota";
cab2.CarModel = "Prius";

List<TaxiCab> availableCabs = new List<TaxiCab>();
availableCabs.Add( cab1 );
availableCabs.Add( cab2 );

GeoPoint pickupLocation1 = new GeoPoint( 40.750549, -73.994232 );
pickupLocation1.Categories.Add( "Pickups" );
pickupLocation1.Metadata.Add( "AvailableCabs", availableCabs );
Backendless.Geo.AddPoint( pickupLocation1 );  

 

Geofence Designer

About Geofencing

Geofencing on the surface, comprises drawing a stationary geometric boundary around an area on a map. This action creates programmatically a set of simple or complex global coordinates which represent a shape. A boundary represents a "fence," which surrounds the area. For Backendless, the boundary and area become meaningful when a Geopoint crosses the boundary or stays within the area.

 

Geofences work with Geopoints. A Geopoint is the most elementary Geolocation concept in Backendless. It is a point (latitude and longitude coordinate pairs) on the map that is accessible via API and allowed to move (change coordinates), i.e., a user carrying a mobile device. In addition to the coordinates, the Geopoint includes metadata in context for the Geopoint.

 

The Geofence Designer

Geofence Designer is a feature of the Geolocation service available under Geolocation > Geofencing. It is a "design time" tool for drawing Geofences on an interactive global map and associating them with events and actions, which can be triggered based on the location of registered Geopoints. Backendless integrates the designer with Google MapsTM, enabling the developer to design precise Geofence positions, locations, and shapes.

 

The Design Tool includes line, rectangle, and circle drawing tools (the cursor changes from hand to crosshairs when selected) for creating Geofence boundaries. Boundaries can be geometrically symmetrical or irregular shapes and have no minimum or maximum size constraints. In use cases, a Geofence conceptually "fences in" a city block, a shopping center, a sports stadium, or perhaps a mall; even smaller geographic areas are possible such as the shoe department in a retail store.

 

fencing11.zoom80

The Line Tool

To define an irregular Geofence, the line tool draws editable line segments and control points. For example, in the United States, the shape of Nevada is irregular. To create this shape, select the line tool to start drawing lines around it.

 

Place the cursor on the map where the first control point should be and click. Drag the cursor to the next place and Click again. The first line segment appears. Repeat these steps until you have nearly completed the shape of your boundary. (It's not a Geofence just yet.) Click the cursor on the last control point (which was the first one set). Backendless detects a closed shape and enables a new Geofence.

 

NOTE: If you accidentally close the Geofence before completing the drawing, the New Geofence dialog box appears. If you click Cancel, the Geofence will be removed. To keep the Geofence, click Save, then re-edit the shape as needed.

geofence-screen1.zoom50

 

Immediately after the shape closes, a popup appears prompting you to name the Geofence. Enter a name in the Geofence Name text box. Since this example uses the state of Nevada, it makes sense to name it Nevada. Click Save to enable the Geofence. (We will refer to this example again.)

geofence-screen2

 

The result is a new item row in the List of Geofences. The Geofence area is filled with green, and the item row is highlighted in yellow when a Geofence is selected on the map or on the list. See the image below.

geofence-screen3.zoom80

The Rectangle Tool

The Rectangle Tool is self-describing. A fence can be drawn quickly around a square or rectangular area. After the Geofence is named and saved, the shape aspect can be adjusted by dragging a line segment or corner control point.  Like the line tool, parameters can be entered in a dialog immediately after the shape is drawn. Should you need to edit the shape, an undo tool appears and to restore the previous edit.

 

fencing13

The Circle Tool

The Circle Tool is also self-describing. A circle can be drawn quickly around an area, repositioned, and resized. Like the line tool, parameters can be entered in a dialog immediately after the shape is drawn.

Deleting a Geofence

A Delete button is positioned directly below the interactive map. For each selected checkbox next to the Geofence name, the Delete button removes those Geofences. Once a Geofence is deleted, it cannot be restored.

geofence-screen4

List of Geofences and Locator Tool

A map filled with Geofences can appear cluttered, especially if the design comprises numerous shapes across several remotely located areas. Backendless organizes the boundary data and actions in a table format below the map, the List of Geofences. The table contains a row for each Geofence along with parameter controls.

 

From left to right, the second column shows the Geofence name, which is hyperlinked to the Update Geofence dialog. The Geofence locator icon is next to the Geofence hyperlink (see image below). The tool repositions the  map view to an optimal zoom-level, from which the Geofence boundary can be easily viewed, accessed, and edited.

geofence-screen5.zoom60

Geofence and Geopoints Events

Geofences and Geopoints are integrated entities of Geolocation. As such, Backendless tracks three specific events:

 

When a Geopoint enters a Geofence, crosses the boundary, On Enter action executes.
When a Geopoint stays inside a shape for a preconfigured amount of time, On Stay actions executes.
When a Geopoint exits the Geofence, it crosses the boundary and is outside the shape, On Exit action executes.

Geopoint Qualification Criteria: Exclusion/Inclusion

Tracking every Geopoint within a Geofence is not desirable in every case. A Geofence plan could specify tracking only Geopoints of a certain nature, for example, visitors or preferred customers.

 

Qualification Criteria, which identifies the types of Geopoints Backendless traces and tracks for a specific geogence, can be defined in the Update Geofence dialog. The Geofence hyperlink opens the dialog. A criterion, in this case, is a special string entered in the Geopoint Qualification Criteria text box. (The string format is the SQL 92 syntax, regular SQL as relates to a database query WHERE clause.) For example, if tracking only visitors, the SQL would need something like usertype=visitor. Where usertype is a Geopoints metadata property. visitor is the metadata property value.

geofence-screen6

Events and Actions

Detecting, tracing, and tracking geopoints in relationship to a geofence establish the event clockwork for execution of developer defined actions. An event occurs as a Geopoint transports into and out of or stays in a Geofence. An action is a set of parameters the developer selects to perform a function, such as deliver a message to a mobile device or add a record to a database.

Events

Three event types are organized in columns in the List of Geofences. The events types are:

 

On Enter - a Geopoint crosses the Geofence boundary into the defined area
On Stay - a Geopoint remains in the Geofence area for at least a specified period
On Exit - a Geopoint crosses the Geofence boundary out of the defined area

Actions

For each of the above events, a developer can select an action and specify parameters to be executed from Backendless. There are four action types:
 

Push notification
Publish-subscribe (pub/sub) message
Send a custom event
Call a URL

 

The scenarios for choosing an action are wildly different; however, they drive the action and parameter choices the developer makes. When an action type is selected for an event, a dialog appears where action parameters can be entered. The fields in the dialog are specific to the action type.

 

fencing-17.zoom30

fencing-18.zoom30

fencing-19.zoom30

fencing-20.zoom30

 

Whenever an action is configured, visual elements indicate whether the parameters are complete. A gear icon and green checkmark indicate proper configuration, where as a red X in place of the checkmark indicates improper configuration. The configuration can be edited by clicking the gear to reopen the currently assigned action dialog.

 

Push Notification Action

All three Geofence events can trigger this action. Push Notification, in basic form, is a message sent to a mobile device associated with a Geopoint or to a group of devices registered with a channel. The Configure Push Notification Action dialog provides flexible parameter options:

 

Content Configuration - configure Push Notification content look and feel for Android, iOS, or Windows Phone.
Message Headers - allows header name and header value.
Delivery - to individual Geopoints or those registered to a channel.

 

fencing-17

Send a Pub/Sub Message Action

This action sends a publish/subscribe message. The developer enters the message parameters in the Configure Pub/Sub Message Action dialog. (Learn more about Message Publishing.) The dialog contains the following fields:

 

Channel name - the name for a channel. Backendless creates the channel if it doesn't exist.
Topic name - the name of a topic used for filtered delivery.
Message headers - optional. Use the key=value format. Comma delimited.
Message body - written in JSON. The body of the message to be delivered.

fencing-18

 

_img2

A probation officer (PO) issues an ankle bracelet to a probationer. He needs to set area from which the probationer cannot leave, such as restricted to travel only within a state. The PO would set up a Geofence outlining Colorado's state-line borders. For that Geofence, he would set the action event On Exit to send a notif