/// <summary> /// Constructor. /// </summary> /// <param name="entityType">The entity type.</param> /// <param name="typeIdentifier">The entity's application domain unique identifier string (or <c>null</c>).</param> /// <param name="creator">The entity creation delegate.</param> public EntityRegistration(Type entityType, string typeIdentifier, EntityCreateDelegate creator) { Covenant.Requires <ArgumentNullException>(entityType != null); Covenant.Requires <ArgumentNullException>(creator != null); this.EntityType = entityType; this.TypeIdentifier = typeIdentifier; this.Creator = creator; }
/// <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); }