using LeanCloud.Storage.Internal; using LeanCloud.Utilities; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Net; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using System.Collections; namespace LeanCloud { /// /// The AVObject is a local representation of data that can be saved and /// retrieved from the LeanCloud cloud. /// /// /// The basic workflow for creating new data is to construct a new AVObject, /// use the indexer to fill it with data, and then use SaveAsync() to persist to the /// database. /// /// /// The basic workflow for accessing existing data is to use a AVQuery /// to specify which existing data to retrieve. /// /// public class AVObject : IEnumerable>, INotifyPropertyChanged, INotifyPropertyUpdated, INotifyCollectionPropertyUpdated, IAVObject { private static readonly string AutoClassName = "_Automatic"; #if UNITY private static readonly bool isCompiledByIL2CPP = AppDomain.CurrentDomain.FriendlyName.Equals("IL2CPP Root Domain"); #else private static readonly bool isCompiledByIL2CPP = false; #endif internal readonly object mutex = new object(); private readonly LinkedList> operationSetQueue = new LinkedList>(); private readonly IDictionary estimatedData = new Dictionary(); private static readonly ThreadLocal isCreatingPointer = new ThreadLocal(() => false); private bool hasBeenFetched; private bool dirty; internal TaskQueue taskQueue = new TaskQueue(); private IObjectState state; internal void MutateState(Action func) { lock (mutex) { state = state.MutatedClone(func); // Refresh the estimated data. RebuildEstimatedData(); } } public IObjectState State { get { return state; } } internal static IAVObjectController ObjectController { get { return AVPlugins.Instance.ObjectController; } } internal static IObjectSubclassingController SubclassingController { get { return AVPlugins.Instance.SubclassingController; } } public static string GetSubClassName() { return SubclassingController.GetClassName(typeof(TAVObject)); } #region AVObject Creation /// /// Constructor for use in AVObject subclasses. Subclasses must specify a AVClassName attribute. /// protected AVObject() : this(AutoClassName) { } /// /// Constructs a new AVObject with no data in it. A AVObject constructed in this way will /// not have an ObjectId and will not persist to the database until /// is called. /// /// /// Class names must be alphanumerical plus underscore, and start with a letter. It is recommended /// to name classes in CamelCaseLikeThis. /// /// The className for this AVObject. public AVObject(string className) { // We use a ThreadLocal rather than passing a parameter so that createWithoutData can do the // right thing with subclasses. It's ugly and terrible, but it does provide the development // experience we generally want, so... yeah. Sorry to whomever has to deal with this in the // future. I pinky-swear we won't make a habit of this -- you believe me, don't you? var isPointer = isCreatingPointer.Value; isCreatingPointer.Value = false; if (className == null) { throw new ArgumentException("You must specify a LeanCloud class name when creating a new AVObject."); } if (AutoClassName.Equals(className)) { className = SubclassingController.GetClassName(GetType()); } // If this is supposed to be created by a factory but wasn't, throw an exception if (!SubclassingController.IsTypeValid(className, GetType())) { throw new ArgumentException( "You must create this type of AVObject using AVObject.Create() or the proper subclass."); } state = new MutableObjectState { ClassName = className }; OnPropertyChanged("ClassName"); operationSetQueue.AddLast(new Dictionary()); if (!isPointer) { hasBeenFetched = true; IsDirty = true; SetDefaultValues(); } else { IsDirty = false; hasBeenFetched = false; } } /// /// Creates a new AVObject based upon a class name. If the class name is a special type (e.g. /// for ), then the appropriate type of AVObject is returned. /// /// The class of object to create. /// A new AVObject for the given class name. public static AVObject Create(string className) { return SubclassingController.Instantiate(className); } /// /// Creates a reference to an existing AVObject for use in creating associations between /// AVObjects. Calling on this object will return /// false until has been called. /// No network request will be made. /// /// The object's class. /// The object id for the referenced object. /// A AVObject without data. public static AVObject CreateWithoutData(string className, string objectId) { isCreatingPointer.Value = true; try { var result = SubclassingController.Instantiate(className); result.ObjectId = objectId; result.IsDirty = false; if (result.IsDirty) { throw new InvalidOperationException( "A AVObject subclass default constructor must not make changes to the object that cause it to be dirty."); } return result; } finally { isCreatingPointer.Value = false; } } /// /// Creates a new AVObject based upon a given subclass type. /// /// A new AVObject for the given class name. public static T Create() where T : AVObject { return (T)SubclassingController.Instantiate(SubclassingController.GetClassName(typeof(T))); } /// /// Creates a reference to an existing AVObject for use in creating associations between /// AVObjects. Calling on this object will return /// false until has been called. /// No network request will be made. /// /// The object id for the referenced object. /// A AVObject without data. public static T CreateWithoutData(string objectId) where T : AVObject { return (T)CreateWithoutData(SubclassingController.GetClassName(typeof(T)), objectId); } /// /// restore a AVObject of subclass instance from IObjectState. /// /// IObjectState after encode from Dictionary. /// The name of the subclass. public static T FromState(IObjectState state, string defaultClassName) where T : AVObject { string className = state.ClassName ?? defaultClassName; T obj = (T)CreateWithoutData(className, state.ObjectId); obj.HandleFetchResult(state); return obj; } #endregion public static IDictionary GetPropertyMappings(string className) { return SubclassingController.GetPropertyMappings(className); } private static string GetFieldForPropertyName(string className, string propertyName) { SubclassingController.GetPropertyMappings(className).TryGetValue(propertyName, out string fieldName); return fieldName; } /// /// Sets the value of a property based upon its associated AVFieldName attribute. /// /// The new value. /// The name of the property. /// The type for the property. protected virtual void SetProperty(T value, #if !UNITY [CallerMemberName] string propertyName = null #else string propertyName #endif ) { this[GetFieldForPropertyName(ClassName, propertyName)] = value; } /// /// Gets a relation for a property based upon its associated AVFieldName attribute. /// /// The AVRelation for the property. /// The name of the property. /// The AVObject subclass type of the AVRelation. protected AVRelation GetRelationProperty( #if !UNITY [CallerMemberName] string propertyName = null #else string propertyName #endif ) where T : AVObject { return GetRelation(GetFieldForPropertyName(ClassName, propertyName)); } /// /// Gets the value of a property based upon its associated AVFieldName attribute. /// /// The value of the property. /// The name of the property. /// The return type of the property. protected virtual T GetProperty( #if !UNITY [CallerMemberName] string propertyName = null #else string propertyName #endif ) { return GetProperty(default(T), propertyName); } /// /// Gets the value of a property based upon its associated AVFieldName attribute. /// /// The value of the property. /// The value to return if the property is not present on the AVObject. /// The name of the property. /// The return type of the property. protected virtual T GetProperty(T defaultValue, #if !UNITY [CallerMemberName] string propertyName = null #else string propertyName #endif ) { T result; if (TryGetValue(GetFieldForPropertyName(ClassName, propertyName), out result)) { return result; } return defaultValue; } /// /// Allows subclasses to set values for non-pointer construction. /// internal virtual void SetDefaultValues() { } /// /// Registers a custom subclass type with the LeanCloud SDK, enabling strong-typing of those AVObjects whenever /// they appear. Subclasses must specify the AVClassName attribute, have a default constructor, and properties /// backed by AVObject fields should have AVFieldName attributes supplied. /// /// The AVObject subclass type to register. public static void RegisterSubclass() where T : AVObject, new() { SubclassingController.RegisterSubclass(typeof(T)); } internal static void UnregisterSubclass() where T : AVObject, new() { SubclassingController.UnregisterSubclass(typeof(T)); } /// /// Clears any changes to this object made since the last call to . /// public void Revert() { lock (mutex) { bool wasDirty = CurrentOperations.Count > 0; if (wasDirty) { CurrentOperations.Clear(); RebuildEstimatedData(); OnPropertyChanged("IsDirty"); } } } internal virtual void HandleFetchResult(IObjectState serverState) { lock (mutex) { MergeFromServer(serverState); } } internal void HandleFailedSave( IDictionary operationsBeforeSave) { lock (mutex) { var opNode = operationSetQueue.Find(operationsBeforeSave); var nextOperations = opNode.Next.Value; bool wasDirty = nextOperations.Count > 0; operationSetQueue.Remove(opNode); // Merge the data from the failed save into the next save. foreach (var pair in operationsBeforeSave) { var operation1 = pair.Value; IAVFieldOperation operation2 = null; nextOperations.TryGetValue(pair.Key, out operation2); if (operation2 != null) { operation2 = operation2.MergeWithPrevious(operation1); } else { operation2 = operation1; } nextOperations[pair.Key] = operation2; } if (!wasDirty && nextOperations == CurrentOperations && operationsBeforeSave.Count > 0) { OnPropertyChanged("IsDirty"); } } } internal virtual void HandleSave(IObjectState serverState) { lock (mutex) { var operationsBeforeSave = operationSetQueue.First.Value; operationSetQueue.RemoveFirst(); // Merge the data from the save and the data from the server into serverData. //MutateState(mutableClone => //{ // mutableClone.Apply(operationsBeforeSave); //}); state = state.MutatedClone((objectState) => objectState.Apply(operationsBeforeSave)); MergeFromServer(serverState); } } public virtual void MergeFromServer(IObjectState serverState) { // Make a new serverData with fetched values. var newServerData = serverState.ToDictionary(t => t.Key, t => t.Value); lock (mutex) { // Trigger handler based on serverState if (serverState.ObjectId != null) { // If the objectId is being merged in, consider this object to be fetched. hasBeenFetched = true; OnPropertyChanged("IsDataAvailable"); } if (serverState.UpdatedAt != null) { OnPropertyChanged("UpdatedAt"); } if (serverState.CreatedAt != null) { OnPropertyChanged("CreatedAt"); } // We cache the fetched object because subsequent Save operation might flush // the fetched objects into Pointers. IDictionary fetchedObject = CollectFetchedObjects(); foreach (var pair in serverState) { var value = pair.Value; if (value is AVObject) { // Resolve fetched object. var avObject = value as AVObject; if (fetchedObject.ContainsKey(avObject.ObjectId)) { value = fetchedObject[avObject.ObjectId]; } } newServerData[pair.Key] = value; } IsDirty = false; serverState = serverState.MutatedClone(mutableClone => { mutableClone.ServerData = newServerData; }); MutateState(mutableClone => { mutableClone.Apply(serverState); }); } } internal void MergeFromObject(AVObject other) { lock (mutex) { // If they point to the same instance, we don't need to merge if (this == other) { return; } } // Clear out any changes on this object. if (operationSetQueue.Count != 1) { throw new InvalidOperationException("Attempt to MergeFromObject during save."); } operationSetQueue.Clear(); foreach (var operationSet in other.operationSetQueue) { operationSetQueue.AddLast(operationSet.ToDictionary(entry => entry.Key, entry => entry.Value)); } lock (mutex) { state = other.State; } RebuildEstimatedData(); } private bool HasDirtyChildren { get { lock (mutex) { return FindUnsavedChildren().FirstOrDefault() != null; } } } /// /// Flattens dictionaries and lists into a single enumerable of all contained objects /// that can then be queried over. /// /// The root of the traversal /// Whether to traverse into AVObjects' children /// Whether to include the root in the result /// internal static IEnumerable DeepTraversal( object root, bool traverseAVObjects = false, bool yieldRoot = false) { var items = DeepTraversalInternal(root, traverseAVObjects, new HashSet(new IdentityEqualityComparer())); if (yieldRoot) { return new[] { root }.Concat(items); } else { return items; } } private static IEnumerable DeepTraversalInternal( object root, bool traverseAVObjects, ICollection seen) { seen.Add(root); var itemsToVisit = isCompiledByIL2CPP ? (System.Collections.IEnumerable)null : (IEnumerable)null; var dict = Conversion.As>(root); if (dict != null) { itemsToVisit = dict.Values; } else { var list = Conversion.As>(root); if (list != null) { itemsToVisit = list; } else if (traverseAVObjects) { var obj = root as AVObject; if (obj != null) { itemsToVisit = obj.Keys.ToList().Select(k => obj[k]); } } } if (itemsToVisit != null) { foreach (var i in itemsToVisit) { if (!seen.Contains(i)) { yield return i; var children = DeepTraversalInternal(i, traverseAVObjects, seen); foreach (var child in children) { yield return child; } } } } } private IEnumerable FindUnsavedChildren() { return DeepTraversal(estimatedData) .OfType() .Where(o => o.IsDirty); } /// /// Deep traversal of this object to grab a copy of any object referenced by this object. /// These instances may have already been fetched, and we don't want to lose their data when /// refreshing or saving. /// /// Map of objectId to AVObject which have been fetched. private IDictionary CollectFetchedObjects() { return DeepTraversal(estimatedData) .OfType() .Where(o => o.ObjectId != null && o.IsDataAvailable) .GroupBy(o => o.ObjectId) .ToDictionary(group => group.Key, group => group.Last()); } public static IDictionary ToJSONObjectForSaving( IDictionary operations) { var result = new Dictionary(); foreach (var pair in operations) { // AVRPCSerialize the data var operation = pair.Value; result[pair.Key] = PointerOrLocalIdEncoder.Instance.Encode(operation); } return result; } internal IDictionary EncodeForSaving(IDictionary data) { var result = new Dictionary(); lock (this.mutex) { foreach (var key in data.Keys) { var value = data[key]; result.Add(key, PointerOrLocalIdEncoder.Instance.Encode(value)); } } return result; } internal IDictionary ServerDataToJSONObjectForSerialization() { return PointerOrLocalIdEncoder.Instance.Encode(state.ToDictionary(t => t.Key, t => t.Value)) as IDictionary; } #region Save Object(s) /// /// Pushes new operations onto the queue and returns the current set of operations. /// public IDictionary StartSave() { lock (mutex) { var currentOperations = CurrentOperations; operationSetQueue.AddLast(new Dictionary()); OnPropertyChanged("IsDirty"); return currentOperations; } } protected virtual Task SaveAsync(Task toAwait, CancellationToken cancellationToken) { IDictionary currentOperations = null; if (!IsDirty) { return Task.FromResult(0); } Task deepSaveTask; string sessionToken; lock (mutex) { // Get the JSON representation of the object. currentOperations = StartSave(); sessionToken = AVUser.CurrentSessionToken; deepSaveTask = DeepSaveAsync(estimatedData, sessionToken, cancellationToken); } return deepSaveTask.OnSuccess(_ => { return toAwait; }).Unwrap().OnSuccess(_ => { return ObjectController.SaveAsync(state, currentOperations, sessionToken, cancellationToken); }).Unwrap().ContinueWith(t => { if (t.IsFaulted || t.IsCanceled) { HandleFailedSave(currentOperations); } else { var serverState = t.Result; HandleSave(serverState); } return t; }).Unwrap(); } /// /// Saves this object to the server. /// public virtual Task SaveAsync() { return SaveAsync(CancellationToken.None); } /// /// Saves this object to the server. /// /// The cancellation token. public virtual Task SaveAsync(CancellationToken cancellationToken) { return taskQueue.Enqueue(toAwait => SaveAsync(toAwait, cancellationToken), cancellationToken); } internal virtual Task FetchAsyncInternal( Task toAwait, IDictionary queryString, CancellationToken cancellationToken) { return toAwait.OnSuccess(_ => { if (ObjectId == null) { throw new InvalidOperationException("Cannot refresh an object that hasn't been saved to the server."); } if (queryString == null) { queryString = new Dictionary(); } return ObjectController.FetchAsync(state, queryString, AVUser.CurrentSessionToken, cancellationToken); }).Unwrap().OnSuccess(t => { HandleFetchResult(t.Result); return this; }); } private static Task DeepSaveAsync(object obj, string sessionToken, CancellationToken cancellationToken) { var objects = new List(); CollectDirtyChildren(obj, objects); var uniqueObjects = new HashSet(objects, new IdentityEqualityComparer()); var saveDirtyFileTasks = DeepTraversal(obj, true) .OfType() .Where(f => f.IsDirty) .Select(f => f.SaveAsync(cancellationToken)).ToList(); return Task.WhenAll(saveDirtyFileTasks).OnSuccess(_ => { IEnumerable remaining = new List(uniqueObjects); return InternalExtensions.WhileAsync(() => Task.FromResult(remaining.Any()), () => { // Partition the objects into two sets: those that can be saved immediately, // and those that rely on other objects to be created first. var current = (from item in remaining where item.CanBeSerialized select item).ToList(); var nextBatch = (from item in remaining where !item.CanBeSerialized select item).ToList(); remaining = nextBatch; if (current.Count == 0) { // We do cycle-detection when building the list of objects passed to this // function, so this should never get called. But we should check for it // anyway, so that we get an exception instead of an infinite loop. throw new InvalidOperationException( "Unable to save a AVObject with a relation to a cycle."); } // Save all of the objects in current. return AVObject.EnqueueForAll(current, toAwait => { return toAwait.OnSuccess(__ => { var states = (from item in current select item.state).ToList(); var operationsList = (from item in current select item.StartSave()).ToList(); var saveTasks = ObjectController.SaveAllAsync(states, operationsList, sessionToken, cancellationToken); return Task.WhenAll(saveTasks).ContinueWith(t => { if (t.IsFaulted || t.IsCanceled) { foreach (var pair in current.Zip(operationsList, (item, ops) => new { item, ops })) { pair.item.HandleFailedSave(pair.ops); } } else { var serverStates = t.Result; foreach (var pair in current.Zip(serverStates, (item, state) => new { item, state })) { pair.item.HandleSave(pair.state); } } cancellationToken.ThrowIfCancellationRequested(); return t; }).Unwrap(); }).Unwrap().OnSuccess(t => (object)null); }, cancellationToken); }); }).Unwrap(); } /// /// Saves each object in the provided list. /// /// The objects to save. public static Task SaveAllAsync(IEnumerable objects) where T : AVObject { return SaveAllAsync(objects, CancellationToken.None); } /// /// Saves each object in the provided list. /// /// The objects to save. /// The cancellation token. public static Task SaveAllAsync( IEnumerable objects, CancellationToken cancellationToken) where T : AVObject { return DeepSaveAsync(objects.ToList(), AVUser.CurrentSessionToken, cancellationToken); } #endregion #region Fetch Object(s) /// /// Fetches this object with the data from the server. /// /// The cancellation token. internal Task FetchAsyncInternal(CancellationToken cancellationToken) { return FetchAsyncInternal(null, cancellationToken); } internal Task FetchAsyncInternal(IDictionary queryString, CancellationToken cancellationToken) { return taskQueue.Enqueue(toAwait => FetchAsyncInternal(toAwait, queryString, cancellationToken), cancellationToken); } internal Task FetchIfNeededAsyncInternal( Task toAwait, CancellationToken cancellationToken) { if (!IsDataAvailable) { return FetchAsyncInternal(toAwait, null, cancellationToken); } return Task.FromResult(this); } /// /// If this AVObject has not been fetched (i.e. returns /// false), fetches this object with the data from the server. /// /// The cancellation token. internal Task FetchIfNeededAsyncInternal(CancellationToken cancellationToken) { return taskQueue.Enqueue(toAwait => FetchIfNeededAsyncInternal(toAwait, cancellationToken), cancellationToken); } /// /// Fetches all of the objects that don't have data in the provided list. /// /// The list passed in for convenience. public static Task> FetchAllIfNeededAsync( IEnumerable objects) where T : AVObject { return FetchAllIfNeededAsync(objects, CancellationToken.None); } /// /// Fetches all of the objects that don't have data in the provided list. /// /// The objects to fetch. /// The cancellation token. /// The list passed in for convenience. public static Task> FetchAllIfNeededAsync( IEnumerable objects, CancellationToken cancellationToken) where T : AVObject { return AVObject.EnqueueForAll(objects.Cast(), (Task toAwait) => { return FetchAllInternalAsync(objects, false, toAwait, cancellationToken); }, cancellationToken); } /// /// Fetches all of the objects in the provided list. /// /// The objects to fetch. /// The list passed in for convenience. public static Task> FetchAllAsync( IEnumerable objects) where T : AVObject { return FetchAllAsync(objects, CancellationToken.None); } /// /// Fetches all of the objects in the provided list. /// /// The objects to fetch. /// The cancellation token. /// The list passed in for convenience. public static Task> FetchAllAsync( IEnumerable objects, CancellationToken cancellationToken) where T : AVObject { return AVObject.EnqueueForAll(objects.Cast(), (Task toAwait) => { return FetchAllInternalAsync(objects, true, toAwait, cancellationToken); }, cancellationToken); } /// /// Fetches all of the objects in the list. /// /// The objects to fetch. /// If false, only objects without data will be fetched. /// A task to await before starting. /// The cancellation token. /// The list passed in for convenience. private static Task> FetchAllInternalAsync( IEnumerable objects, bool force, Task toAwait, CancellationToken cancellationToken) where T : AVObject { return toAwait.OnSuccess(_ => { if (objects.Any(obj => { return obj.state.ObjectId == null; })) { throw new InvalidOperationException("You cannot fetch objects that haven't already been saved."); } var objectsToFetch = (from obj in objects where force || !obj.IsDataAvailable select obj).ToList(); if (objectsToFetch.Count == 0) { return Task.FromResult(objects); } // Do one Find for each class. var findsByClass = (from obj in objectsToFetch group obj.ObjectId by obj.ClassName into classGroup where classGroup.Count() > 0 select new { ClassName = classGroup.Key, FindTask = new AVQuery(classGroup.Key) .WhereContainedIn("objectId", classGroup) .FindAsync(cancellationToken) }).ToDictionary(pair => pair.ClassName, pair => pair.FindTask); // Wait for all the Finds to complete. return Task.WhenAll(findsByClass.Values.ToList()).OnSuccess(__ => { if (cancellationToken.IsCancellationRequested) { return objects; } // Merge the data from the Finds into the input objects. var pairs = from obj in objectsToFetch from result in findsByClass[obj.ClassName].Result where result.ObjectId == obj.ObjectId select new { obj, result }; foreach (var pair in pairs) { pair.obj.MergeFromObject(pair.result); pair.obj.hasBeenFetched = true; } return objects; }); }).Unwrap(); } #endregion #region Delete Object internal Task DeleteAsync(Task toAwait, CancellationToken cancellationToken) { if (ObjectId == null) { return Task.FromResult(0); } string sessionToken = AVUser.CurrentSessionToken; return toAwait.OnSuccess(_ => { return ObjectController.DeleteAsync(State, sessionToken, cancellationToken); }).Unwrap().OnSuccess(_ => IsDirty = true); } /// /// Deletes this object on the server. /// public Task DeleteAsync() { return DeleteAsync(CancellationToken.None); } /// /// Deletes this object on the server. /// /// The cancellation token. public Task DeleteAsync(CancellationToken cancellationToken) { return taskQueue.Enqueue(toAwait => DeleteAsync(toAwait, cancellationToken), cancellationToken); } /// /// Deletes each object in the provided list. /// /// The objects to delete. public static Task DeleteAllAsync(IEnumerable objects) where T : AVObject { return DeleteAllAsync(objects, CancellationToken.None); } /// /// Deletes each object in the provided list. /// /// The objects to delete. /// The cancellation token. public static Task DeleteAllAsync( IEnumerable objects, CancellationToken cancellationToken) where T : AVObject { var uniqueObjects = new HashSet(objects.OfType().ToList(), new IdentityEqualityComparer()); return AVObject.EnqueueForAll(uniqueObjects, toAwait => { var states = uniqueObjects.Select(t => t.state).ToList(); return toAwait.OnSuccess(_ => { var deleteTasks = ObjectController.DeleteAllAsync(states, AVUser.CurrentSessionToken, cancellationToken); return Task.WhenAll(deleteTasks); }).Unwrap().OnSuccess(t => { // Dirty all objects in memory. foreach (var obj in uniqueObjects) { obj.IsDirty = true; } return (object)null; }); }, cancellationToken); } #endregion private static void CollectDirtyChildren(object node, IList dirtyChildren, ICollection seen, ICollection seenNew) { foreach (var obj in DeepTraversal(node).OfType()) { ICollection scopedSeenNew; // Check for cycles of new objects. Any such cycle means it will be impossible to save // this collection of objects, so throw an exception. if (obj.ObjectId != null) { scopedSeenNew = new HashSet(new IdentityEqualityComparer()); } else { if (seenNew.Contains(obj)) { throw new InvalidOperationException("Found a circular dependency while saving"); } scopedSeenNew = new HashSet(seenNew, new IdentityEqualityComparer()); scopedSeenNew.Add(obj); } // Check for cycles of any object. If this occurs, then there's no problem, but // we shouldn't recurse any deeper, because it would be an infinite recursion. if (seen.Contains(obj)) { return; } seen.Add(obj); // Recurse into this object's children looking for dirty children. // We only need to look at the child object's current estimated data, // because that's the only data that might need to be saved now. CollectDirtyChildren(obj.estimatedData, dirtyChildren, seen, scopedSeenNew); if (obj.CheckIsDirty(false)) { dirtyChildren.Add(obj); } } } /// /// Helper version of CollectDirtyChildren so that callers don't have to add the internally /// used parameters. /// private static void CollectDirtyChildren(object node, IList dirtyChildren) { CollectDirtyChildren(node, dirtyChildren, new HashSet(new IdentityEqualityComparer()), new HashSet(new IdentityEqualityComparer())); } /// /// Returns true if the given object can be serialized for saving as a value /// that is pointed to by a AVObject. /// private static bool CanBeSerializedAsValue(object value) { return DeepTraversal(value, yieldRoot: true) .OfType() .All(o => o.ObjectId != null); } private bool CanBeSerialized { get { // This method is only used for batching sets of objects for saveAll // and when saving children automatically. Since it's only used to // determine whether or not save should be called on them, it only // needs to examine their current values, so we use estimatedData. lock (mutex) { return CanBeSerializedAsValue(estimatedData); } } } /// /// Adds a task to the queue for all of the given objects. /// private static Task EnqueueForAll(IEnumerable objects, Func> taskStart, CancellationToken cancellationToken) { // The task that will be complete when all of the child queues indicate they're ready to start. var readyToStart = new TaskCompletionSource(); // First, we need to lock the mutex for the queue for every object. We have to hold this // from at least when taskStart() is called to when obj.taskQueue enqueue is called, so // that saves actually get executed in the order they were setup by taskStart(). // The locks have to be sorted so that we always acquire them in the same order. // Otherwise, there's some risk of deadlock. var lockSet = new LockSet(objects.Select(o => o.taskQueue.Mutex)); lockSet.Enter(); try { // The task produced by taskStart. By running this immediately, we allow everything prior // to toAwait to run before waiting for all of the queues on all of the objects. Task fullTask = taskStart(readyToStart.Task); // Add fullTask to each of the objects' queues. var childTasks = new List(); foreach (AVObject obj in objects) { obj.taskQueue.Enqueue((Task task) => { childTasks.Add(task); return fullTask; }, cancellationToken); } // When all of the objects' queues are ready, signal fullTask that it's ready to go on. Task.WhenAll(childTasks.ToArray()).ContinueWith((Task task) => { readyToStart.SetResult(null); }); return fullTask; } finally { lockSet.Exit(); } } /// /// Removes a key from the object's data if it exists. /// /// The key to remove. public virtual void Remove(string key) { lock (mutex) { CheckKeyIsMutable(key); PerformOperation(key, AVDeleteOperation.Instance); } } private IEnumerable ApplyOperations(IDictionary operations, IDictionary map) { List appliedKeys = new List(); lock (mutex) { foreach (var pair in operations) { object oldValue; map.TryGetValue(pair.Key, out oldValue); var newValue = pair.Value.Apply(oldValue, pair.Key); if (newValue != AVDeleteOperation.DeleteToken) { map[pair.Key] = newValue; } else { map.Remove(pair.Key); } appliedKeys.Add(pair.Key); } } return appliedKeys; } /// /// Regenerates the estimatedData map from the serverData and operations. /// internal void RebuildEstimatedData() { IEnumerable changedKeys = null; lock (mutex) { //estimatedData.Clear(); List converdKeys = new List(); foreach (var item in state) { var key = item.Key; var value = item.Value; if (!estimatedData.ContainsKey(key)) { converdKeys.Add(key); } else { var oldValue = estimatedData[key]; if (oldValue != value) { converdKeys.Add(key); } estimatedData.Remove(key); } estimatedData.Add(item); } changedKeys = converdKeys; foreach (var operations in operationSetQueue) { var appliedKeys = ApplyOperations(operations, estimatedData); changedKeys = converdKeys.Concat(appliedKeys); } // We've just applied a bunch of operations to estimatedData which // may have changed all of its keys. Notify of all keys and properties // mapped to keys being changed. OnFieldsChanged(changedKeys); } } /// /// PerformOperation is like setting a value at an index, but instead of /// just taking a new value, it takes a AVFieldOperation that modifies the value. /// internal void PerformOperation(string key, IAVFieldOperation operation) { lock (mutex) { var ifDirtyBeforePerform = this.IsDirty; object oldValue; estimatedData.TryGetValue(key, out oldValue); object newValue = operation.Apply(oldValue, key); if (newValue != AVDeleteOperation.DeleteToken) { estimatedData[key] = newValue; } else { estimatedData.Remove(key); } IAVFieldOperation oldOperation; bool wasDirty = CurrentOperations.Count > 0; CurrentOperations.TryGetValue(key, out oldOperation); var newOperation = operation.MergeWithPrevious(oldOperation); CurrentOperations[key] = newOperation; if (!wasDirty) { OnPropertyChanged("IsDirty"); if (ifDirtyBeforePerform != wasDirty) { OnPropertyUpdated("IsDirty", ifDirtyBeforePerform, wasDirty); } } OnFieldsChanged(new[] { key }); OnPropertyUpdated(key, oldValue, newValue); } } /// /// Override to run validations on key/value pairs. Make sure to still /// call the base version. /// internal virtual void OnSettingValue(ref string key, ref object value) { if (key == null) { throw new ArgumentNullException("key"); } } /// /// Gets or sets a value on the object. It is recommended to name /// keys in partialCamelCaseLikeThis. /// /// The key for the object. Keys must be alphanumeric plus underscore /// and start with a letter. /// The property is /// retrieved and is not found. /// The value for the key. virtual public object this[string key] { get { lock (mutex) { CheckGetAccess(key); var value = estimatedData[key]; // A relation may be deserialized without a parent or key. Either way, // make sure it's consistent. var relation = value as AVRelationBase; if (relation != null) { relation.EnsureParentAndKey(this, key); } return value; } } set { lock (mutex) { CheckKeyIsMutable(key); Set(key, value); } } } /// /// Perform Set internally which is not gated by mutability check. /// /// key for the object. /// the value for the key. internal void Set(string key, object value) { lock (mutex) { OnSettingValue(ref key, ref value); if (!AVEncoder.IsValidType(value)) { throw new ArgumentException("Invalid type for value: " + value.GetType().ToString()); } PerformOperation(key, new AVSetOperation(value)); } } internal void SetIfDifferent(string key, T value) { T current; bool hasCurrent = TryGetValue(key, out current); if (value == null) { if (hasCurrent) { PerformOperation(key, AVDeleteOperation.Instance); } return; } if (!hasCurrent || !value.Equals(current)) { Set(key, value); } } #region Atomic Increment /// /// Atomically increments the given key by 1. /// /// The key to increment. public void Increment(string key) { Increment(key, 1); } /// /// Atomically increments the given key by the given number. /// /// The key to increment. /// The amount to increment by. public void Increment(string key, long amount) { lock (mutex) { CheckKeyIsMutable(key); PerformOperation(key, new AVIncrementOperation(amount)); } } /// /// Atomically increments the given key by the given number. /// /// The key to increment. /// The amount to increment by. public void Increment(string key, double amount) { lock (mutex) { CheckKeyIsMutable(key); PerformOperation(key, new AVIncrementOperation(amount)); } } #endregion /// /// Atomically adds an object to the end of the list associated with the given key. /// /// The key. /// The object to add. public void AddToList(string key, object value) { AddRangeToList(key, new[] { value }); } /// /// Atomically adds objects to the end of the list associated with the given key. /// /// The key. /// The objects to add. public void AddRangeToList(string key, IEnumerable values) { lock (mutex) { CheckKeyIsMutable(key); PerformOperation(key, new AVAddOperation(values.Cast())); OnCollectionPropertyUpdated(key, NotifyCollectionUpdatedAction.Add, null, values); } } /// /// Atomically adds an object to the end of the list associated with the given key, /// only if it is not already present in the list. The position of the insert is not /// guaranteed. /// /// The key. /// The object to add. public void AddUniqueToList(string key, object value) { AddRangeUniqueToList(key, new object[] { value }); } /// /// Atomically adds objects to the end of the list associated with the given key, /// only if they are not already present in the list. The position of the inserts are not /// guaranteed. /// /// The key. /// The objects to add. public void AddRangeUniqueToList(string key, IEnumerable values) { lock (mutex) { CheckKeyIsMutable(key); PerformOperation(key, new AVAddUniqueOperation(values.Cast())); } } /// /// Atomically removes all instances of the objects in /// from the list associated with the given key. /// /// The key. /// The objects to remove. public void RemoveAllFromList(string key, IEnumerable values) { lock (mutex) { CheckKeyIsMutable(key); PerformOperation(key, new AVRemoveOperation(values.Cast())); OnCollectionPropertyUpdated(key, NotifyCollectionUpdatedAction.Remove, values, null); } } /// /// Returns whether this object has a particular key. /// /// The key to check for public bool ContainsKey(string key) { lock (mutex) { return estimatedData.ContainsKey(key); } } /// /// Gets a value for the key of a particular type. /// The type to convert the value to. Supported types are /// AVObject and its descendents, LeanCloud types such as AVRelation and AVGeopoint, /// primitive types,IList<T>, IDictionary<string, T>, and strings. /// The key of the element to get. /// The property is /// retrieved and is not found. /// public T Get(string key) { return Conversion.To(this[key]); } /// /// Access or create a Relation value for a key. /// /// The type of object to create a relation for. /// The key for the relation field. /// A AVRelation for the key. public AVRelation GetRelation(string key) where T : AVObject { // All the sanity checking is done when add or remove is called. AVRelation relation = null; TryGetValue(key, out relation); return relation ?? new AVRelation(this, key); } /// /// Get relation revserse query. /// /// AVObject /// parent className /// key /// public AVQuery GetRelationRevserseQuery(string parentClassName, string key) where T : AVObject { if (string.IsNullOrEmpty(parentClassName)) { throw new ArgumentNullException("parentClassName", "can not query a relation without parentClassName."); } if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException("key", "can not query a relation without key."); } return new AVQuery(parentClassName).WhereEqualTo(key, this); } /// /// Populates result with the value for the key, if possible. /// /// The desired type for the value. /// The key to retrieve a value for. /// The value for the given key, converted to the /// requested type, or null if unsuccessful. /// true if the lookup and conversion succeeded, otherwise /// false. public virtual bool TryGetValue(string key, out T result) { lock (mutex) { if (ContainsKey(key)) { try { var temp = Conversion.To(this[key]); result = temp; return true; } catch (InvalidCastException) { result = default(T); return false; } } result = default(T); return false; } } /// /// Gets whether the AVObject has been fetched. /// public bool IsDataAvailable { get { lock (mutex) { return hasBeenFetched; } } } private bool CheckIsDataAvailable(string key) { lock (mutex) { return IsDataAvailable || estimatedData.ContainsKey(key); } } private void CheckGetAccess(string key) { lock (mutex) { if (!CheckIsDataAvailable(key)) { throw new InvalidOperationException( "AVObject has no data for this key. Call FetchIfNeededAsync() to get the data."); } } } private void CheckKeyIsMutable(string key) { if (!IsKeyMutable(key)) { throw new InvalidOperationException( "Cannot change the `" + key + "` property of a `" + ClassName + "` object."); } } protected virtual bool IsKeyMutable(string key) { return true; } /// /// A helper function for checking whether two AVObjects point to /// the same object in the cloud. /// public bool HasSameId(AVObject other) { lock (mutex) { return other != null && object.Equals(ClassName, other.ClassName) && object.Equals(ObjectId, other.ObjectId); } } internal IDictionary CurrentOperations { get { lock (mutex) { return operationSetQueue.Last.Value; } } } /// /// Gets a set view of the keys contained in this object. This does not include createdAt, /// updatedAt, or objectId. It does include things like username and ACL. /// public ICollection Keys { get { lock (mutex) { return estimatedData.Keys; } } } /// /// Gets or sets the AVACL governing this object. /// [AVFieldName("ACL")] public AVACL ACL { get { return GetProperty(null, "ACL"); } set { SetProperty(value, "ACL"); } } /// /// Returns true if this object was created by the LeanCloud server when the /// object might have already been there (e.g. in the case of a Facebook /// login) /// #if !UNITY public #else internal #endif bool IsNew { get { return state.IsNew; } #if !UNITY internal #endif set { MutateState(mutableClone => { mutableClone.IsNew = value; }); OnPropertyChanged("IsNew"); } } /// /// Gets the last time this object was updated as the server sees it, so that if you make changes /// to a AVObject, then wait a while, and then call , the updated time /// will be the time of the call rather than the time the object was /// changed locally. /// [AVFieldName("updatedAt")] public DateTime? UpdatedAt { get { return state.UpdatedAt; } } /// /// Gets the first time this object was saved as the server sees it, so that if you create a /// AVObject, then wait a while, and then call , the /// creation time will be the time of the first call rather than /// the time the object was created locally. /// [AVFieldName("createdAt")] public DateTime? CreatedAt { get { return state.CreatedAt; } } /// /// Indicates whether this AVObject has unsaved changes. /// public bool IsDirty { get { lock (mutex) { return CheckIsDirty(true); } } internal set { lock (mutex) { dirty = value; OnPropertyChanged("IsDirty"); } } } /// /// Indicates whether key is unsaved for this AVObject. /// /// The key to check for. /// true if the key has been altered and not saved yet, otherwise /// false. public bool IsKeyDirty(string key) { lock (mutex) { return CurrentOperations.ContainsKey(key); } } private bool CheckIsDirty(bool considerChildren) { lock (mutex) { return (dirty || CurrentOperations.Count > 0 || (considerChildren && HasDirtyChildren)); } } /// /// Gets or sets the object id. An object id is assigned as soon as an object is /// saved to the server. The combination of a and an /// uniquely identifies an object in your application. /// [AVFieldName("objectId")] public string ObjectId { get { return state.ObjectId; } set { IsDirty = true; SetObjectIdInternal(value); } } /// /// Sets the objectId without marking dirty. /// /// The new objectId private void SetObjectIdInternal(string objectId) { lock (mutex) { MutateState(mutableClone => { mutableClone.ObjectId = objectId; }); OnPropertyChanged("ObjectId"); } } /// /// Gets the class name for the AVObject. /// public string ClassName { get { return state.ClassName; } } /// /// Adds a value for the given key, throwing an Exception if the key /// already has a value. /// /// /// This allows you to use collection initialization syntax when creating AVObjects, /// such as: /// /// var obj = new AVObject("MyType") /// { /// {"name", "foo"}, /// {"count", 10}, /// {"found", false} /// }; /// /// /// The key for which a value should be set. /// The value for the key. public void Add(string key, object value) { lock (mutex) { if (this.ContainsKey(key)) { throw new ArgumentException("Key already exists", key); } this[key] = value; } } IEnumerator> IEnumerable> .GetEnumerator() { lock (mutex) { return estimatedData.GetEnumerator(); } } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { lock (mutex) { return ((IEnumerable>)this).GetEnumerator(); } } /// /// Gets a for the type of object specified by /// /// /// The class name of the object. /// A new . public static AVQuery GetQuery(string className) { // Since we can't return a AVQuery (due to strong-typing with // generics), we'll require you to go through subclasses. This is a better // experience anyway, especially with LINQ integration, since you'll get // strongly-typed queries and compile-time checking of property names and // types. if (SubclassingController.GetType(className) != null) { throw new ArgumentException( "Use the class-specific query properties for class " + className, "className"); } return new AVQuery(className); } /// /// Raises change notifications for all properties associated with the given /// field names. If fieldNames is null, this will notify for all known field-linked /// properties (e.g. this happens when we recalculate all estimated data from scratch) /// protected virtual void OnFieldsChanged(IEnumerable fieldNames) { var mappings = SubclassingController.GetPropertyMappings(ClassName); IEnumerable properties; if (fieldNames != null && mappings != null) { properties = from m in mappings join f in fieldNames on m.Value equals f select m.Key; } else if (mappings != null) { properties = mappings.Keys; } else { properties = Enumerable.Empty(); } foreach (var property in properties) { OnPropertyChanged(property); } OnPropertyChanged("Item[]"); } /// /// Raises change notifications for a property. Passing null or the empty string /// notifies the binding framework that all properties/indexes have changed. /// Passing "Item[]" tells the binding framework that all indexed values /// have changed (but not all properties) /// protected virtual void OnPropertyChanged( #if !UNITY [CallerMemberName] string propertyName = null #else string propertyName #endif ) { propertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); } private SynchronizedEventHandler propertyChanged = new SynchronizedEventHandler(); /// /// Occurs when a property value changes. /// public event PropertyChangedEventHandler PropertyChanged { add { propertyChanged.Add(value); } remove { propertyChanged.Remove(value); } } private SynchronizedEventHandler propertyUpdated = new SynchronizedEventHandler(); public event PropertyUpdatedEventHandler PropertyUpdated { add { propertyUpdated.Add(value); } remove { propertyUpdated.Remove(value); } } protected virtual void OnPropertyUpdated(string propertyName, object newValue, object oldValue) { propertyUpdated.Invoke(this, new PropertyUpdatedEventArgs(propertyName, oldValue, newValue)); } private SynchronizedEventHandler collectionUpdated = new SynchronizedEventHandler(); public event CollectionPropertyUpdatedEventHandler CollectionPropertyUpdated { add { collectionUpdated.Add(value); } remove { collectionUpdated.Remove(value); } } protected virtual void OnCollectionPropertyUpdated(string propertyName, NotifyCollectionUpdatedAction action, IEnumerable oldValues, IEnumerable newValues) { collectionUpdated?.Invoke(this, new CollectionPropertyUpdatedEventArgs(propertyName, action, oldValues, newValues)); } } public interface INotifyPropertyUpdated { event PropertyUpdatedEventHandler PropertyUpdated; } public interface INotifyCollectionPropertyUpdated { event CollectionPropertyUpdatedEventHandler CollectionPropertyUpdated; } public enum NotifyCollectionUpdatedAction { Add, Remove } public class CollectionPropertyUpdatedEventArgs : PropertyChangedEventArgs { public CollectionPropertyUpdatedEventArgs(string propertyName, NotifyCollectionUpdatedAction collectionAction, IEnumerable oldValues, IEnumerable newValues) : base(propertyName) { CollectionAction = collectionAction; OldValues = oldValues; NewValues = newValues; } public IEnumerable OldValues { get; set; } public IEnumerable NewValues { get; set; } public NotifyCollectionUpdatedAction CollectionAction { get; set; } } public class PropertyUpdatedEventArgs : PropertyChangedEventArgs { public PropertyUpdatedEventArgs(string propertyName, object oldValue, object newValue) : base(propertyName) { OldValue = oldValue; NewValue = newValue; } public object OldValue { get; private set; } public object NewValue { get; private set; } } public delegate void PropertyUpdatedEventHandler(object sender, PropertyUpdatedEventArgs args); public delegate void CollectionPropertyUpdatedEventHandler(object sender, CollectionPropertyUpdatedEventArgs args); }