private string link; // The entity link string or NULL /// <summary> /// Constructor. /// </summary> /// <param name="propertyNameMap"> /// The dictionary mapping JSON property names to their entity equivalents. /// </param> /// <param name="context">The optional entity context.</param> public DynamicEntity(IDictionary <string, string> propertyNameMap, IDynamicEntityContext context = null) { Covenant.Requires <ArgumentNullException>(propertyNameMap != null); this.propertyNameMap = propertyNameMap; this.context = context; this.self = (IDynamicEntity)this; this.parent = null; }
/// <summary> /// Constructor. /// </summary> /// <param name="parentEntity">The <see cref="IDynamicEntity"/> that owns this mapper.</param> /// <param name="jsonName">The JSON property name.</param> /// <param name="propertyName">The entity property name.</param> /// <param name="context">The <see cref="IDynamicEntityContext"/> or <c>null</c>.</param> public SimpleMapper(IDynamicEntity parentEntity, string jsonName, string propertyName, IDynamicEntityContext context) { Covenant.Requires <ArgumentNullException>(parentEntity != null); Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(jsonName)); Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(propertyName)); this.parentEntity = parentEntity; this.context = context; this.JsonName = jsonName; this.PropertyName = propertyName; this.property = null; }
/// <summary> /// Constructor. /// </summary> /// <param name="parentEntity">The <see cref="IDynamicEntity"/> that owns this list.</param> /// <param name="context">The <see cref="IDynamicEntityContext"/> or <c>null</c>.</param> /// <param name="jArray">The underlying <see cref="jArray"/>.</param> /// <param name="items">The initial items or <c>null</c> to initialize from <paramref name="jArray"/>.</param> public EntityListWrapper(IDynamicEntity parentEntity, IDynamicEntityContext context, JArray jArray, IEnumerable <TEntity> items) { Covenant.Requires <ArgumentNullException>(parentEntity != null); Covenant.Requires <ArgumentNullException>(jArray != null); this.parentEntity = parentEntity; this.context = context; this.jArray = jArray; this.itemChangedHandler = new EventHandler <EventArgs>(OnItemChanged); this.list = new ObservableCollection <TEntity>(); if (items != null) { Covenant.Assert(jArray.Count == 0); foreach (var item in items) { Add(item); } } else { foreach (var jToken in jArray) { if (jToken.Type == JTokenType.Object) { var item = DynamicEntity.Create <TEntity>((JObject)jToken, context); item.Changed += itemChangedHandler; // We need to bubble up nested change events list.Add(item); } else { list.Add(null); // Ignore anything that's not an object. } } } // We're going to listen to our own collection changed event to // bubble them up. list.CollectionChanged += (s, a) => { CollectionChanged?.Invoke(this, a); parentEntity._OnChanged(); }; }
/// <summary> /// Constructor. /// </summary> /// <param name="parentEntity">The <see cref="IDynamicEntity"/> that owns this list.</param> /// <param name="context">The <see cref="IDynamicEntityContext"/> or <c>null</c>.</param> /// <param name="jArray">The underlying <see cref="jArray"/>.</param> /// <param name="items">The initial items or <c>null</c> to initialize from <paramref name="jArray"/>.</param> public LinkListWrapper(IDynamicEntity parentEntity, IDynamicEntityContext context, JArray jArray, IEnumerable <TEntity> items) { Covenant.Requires <ArgumentNullException>(parentEntity != null); Covenant.Requires <ArgumentNullException>(jArray != null); this.parentEntity = parentEntity; this.context = context; this.jArray = jArray; this.list = new ObservableCollection <LinkState>(); if (items != null) { Covenant.Assert(jArray.Count == 0); foreach (var item in items) { Add(item); } } else { foreach (var token in jArray) { list.Add(new LinkState() { Link = GetLink(token) }); } } // We're going to listen to our own collection changed event to // bubble them up. list.CollectionChanged += (s, a) => { if (!notifyDisabled) { CollectionChanged?.Invoke(this, a); parentEntity._OnChanged(); } }; }
/// <summary> /// Constructs a <typeparamref name="TEntity"/> from a <see cref="JObject"/>. /// </summary> /// <typeparam name="TEntity">The entity type.</typeparam> /// <param name="jObject">The backing <see cref="JObject"/>.</param> /// <param name="context">The <see cref="IDynamicEntityContext"/> or <c>null</c>.</param> /// <returns>The new <typeparamref name="TEntity"/>.</returns> /// <remarks> /// <para> /// This method is capable of instantiating derived entities in a future-proof /// manner. This is possible for entities whose defining <c>interface</c>s were /// tagged with a <see cref="DynamicEntityAttribute.Type"/> value. This is /// set to a unique identifier for the type within the application domain. /// </para> /// <para> /// Entities serialize their own type identifier as well as the /// identifiers for any inherited types to the internal <see cref="DynamicEntity.EntityTypePathName"/> /// JSON property. The type identifiers are formatted into a list of /// colon (<b>:</b>) separated values with the current type's identifier /// listed first, followed by its parent's identifier and so on, all the /// way to the root entity's identifier. /// </para> /// <para> /// This identifier list provides the information necessary to dynamically /// instantiate the derived entity that best fits the data. For example, /// say you release a product that defines a base <c>IProduct</c> interface /// with <c>ICandy</c> derived from it. IProduct defines a <b>Name</b> as /// property and ICandy adds <b>Calories</b>. /// </para> /// <para> /// An application can persist a base IProduct instance to the database /// as well as a derived ICandy. Calling this method to load the base /// IProduct or ICandy document will simply load it. Calling this method /// to load IProduct but passing the ICandy document, will actually create /// an ICandy instance but return it cast to the base type (IProduct). /// </para> /// <para> /// Applications can test entity types using the usual C# <c>is</c>/<c>as</c> /// operators or switch on the entity's type property, if one was tagged /// using <see cref="DynamicEntityPropertyAttribute.IsTypeProperty"/>. /// </para> /// <para> /// This also enables future-proofing. Say version 1.0 of an application only /// knew about IProduct and ICandy. Then, version 2.0 is released that adds /// <c>IBeer</c> which also derives from IProduct and persists some beer to /// a database. Loading an IBeer document as an IProduct from version 1.0 /// will still work, even though v1 is not aware of this type. An IProduct /// entity will be returned (what else can be done), but the IBeer properties /// will still be loaded into the underlying <c>JObject</c>. /// </para> /// </remarks> public static TEntity Create <TEntity>(JObject jObject, IDynamicEntityContext context) where TEntity : class, IDynamicEntity, new() { Covenant.Requires <ArgumentNullException>(jObject != null); EntityCreateDelegate creator = null; string[] typePath = null; JToken typePathToken; CreateInfo createInfo; if (jObject.TryGetValue(EntityTypePathName, out typePathToken) && typePathToken.Type == JTokenType.String) { typePath = ((string)typePathToken).Split(colonSplitter); } var hasTypePath = typePath != null && typePath.Length > 0; if (!hasTypePath) { // The document doesn't have a type path property so we're going to // use the creator registered for the type. typeToEntityCreator.TryGetValue(typeof(TEntity), out creator); } else { // Try each type identifier from the beginning of the path and use the // first registered creator we can find. This will return the derived // class that best fits the data. foreach (var typeIdentifier in typePath) { if (typeIdentifierToEntityCreator.TryGetValue(typeIdentifier, out createInfo)) { creator = createInfo.Creator; break; } } } if (creator == null) { if (hasTypePath) { throw new InvalidOperationException($"Entity type [{typeof(TEntity).FullName }] has not registered an entity creation function. Make sure you have registered the generated entities and binder document types."); } else { throw new InvalidOperationException($"Could not map any type from the [{typePath}] type path to an entity type. Make sure you have registered the generated entities and binder document types."); } } var untypedEntity = creator(jObject, context); if (untypedEntity == null) { return(null); } var entity = untypedEntity as TEntity; if (entity == null) { throw new InvalidCastException($"Unable to cast [{untypedEntity.GetType().FullName}] to [{typeof(TEntity).FullName}]. Possible data corruption or invalid type path [{typePath}]."); } return(entity); }