/// <summary> /// Returns the processed value for the given type and property. /// </summary> /// <param name="content">The <see cref="IPublishedContent" /> to convert.</param> /// <param name="culture">The <see cref="CultureInfo" /></param> /// <param name="targetType">The target type.</param> /// <param name="propertyInfo">The <see cref="PropertyInfo" /> property info associated with the type.</param> /// <param name="instance">The instance to assign the value to.</param> /// <param name="contextAccessor">The context accessor.</param> /// <param name="chainContext">The <see cref="DittoChainContext"/> for the current processor chain.</param> /// <returns> /// The <see cref="object" /> representing the Umbraco value. /// </returns> private static object GetProcessedValue( IPublishedContent content, CultureInfo culture, Type targetType, PropertyInfo propertyInfo, object instance, IDittoContextAccessor contextAccessor, DittoChainContext chainContext) { using (DittoDisposableTimer.DebugDuration <Ditto>($"Processing '{propertyInfo.Name}' ({content.Id})")) { // Get the target property description var propertyDescriptor = TypeDescriptor.GetProperties(instance)[propertyInfo.Name]; // Create a base processor context for this current chain level var baseProcessorContext = new DittoProcessorContext { Content = content, TargetType = targetType, PropertyDescriptor = propertyDescriptor, Culture = culture }; // Check for cache attribute var cacheAttr = propertyInfo.GetCustomAttribute <DittoCacheAttribute>(true); if (cacheAttr != null) { var ctx = new DittoCacheContext(cacheAttr, content, targetType, propertyDescriptor, culture); return(cacheAttr.GetCacheItem(ctx, () => DoGetProcessedValue(content, propertyInfo, contextAccessor, baseProcessorContext, chainContext))); } else { return(DoGetProcessedValue(content, propertyInfo, contextAccessor, baseProcessorContext, chainContext)); } } }
/// <summary> /// Returns the processed value for the given type and property. /// </summary> /// <param name="content">The content.</param> /// <param name="propertyInfo">The property information.</param> /// <param name="contextAccessor">The context accessor.</param> /// <param name="baseProcessorContext">The base processor context.</param> /// <param name="chainContext">The <see cref="DittoChainContext"/> for the current processor chain.</param> /// <returns>Returns the processed value.</returns> private static object DoGetProcessedValue( IPublishedContent content, PropertyInfo propertyInfo, IDittoContextAccessor contextAccessor, DittoProcessorContext baseProcessorContext, DittoChainContext chainContext) { // Check the property for any explicit processor attributes var processorAttrs = propertyInfo.GetCustomAttributes <DittoProcessorAttribute>(true) .OrderBy(x => x.Order) .ToList(); if (!processorAttrs.Any()) { // Adds the default processor for this conversion processorAttrs.Add(DittoProcessorRegistry.Instance.GetDefaultProcessorFor(baseProcessorContext.TargetType)); } var propertyType = propertyInfo.PropertyType; // Check for type registered processors processorAttrs.AddRange(propertyType .GetCustomAttributes <DittoProcessorAttribute>(true) .OrderBy(x => x.Order)); // Check any type arguments in generic enumerable types. // This should return false against typeof(string) etc also. var typeInfo = propertyType.GetTypeInfo(); bool isEnumerable = false; Type typeArg = null; if (propertyType.IsCastableEnumerableType()) { typeArg = typeInfo.GenericTypeArguments.First(); processorAttrs.AddRange(typeInfo .GenericTypeArguments .First() .GetCustomAttributes <DittoProcessorAttribute>(true) .OrderBy(x => x.Order) .ToList()); isEnumerable = true; } // Check for globally registered processors processorAttrs.AddRange(DittoProcessorRegistry.Instance.GetRegisteredProcessorAttributesFor(propertyInfo.PropertyType)); // Add any core processors onto the end processorAttrs.AddRange(DittoProcessorRegistry.Instance.GetPostProcessorAttributes()); // Create holder for value as it's processed object currentValue = content; // Process attributes foreach (var processorAttr in processorAttrs) { using (DittoDisposableTimer.DebugDuration <Ditto>($"Processor '{processorAttr.GetType().Name}' ({content.Id})")) { // Get the right context type var ctx = chainContext.ProcessorContexts.GetOrCreate(baseProcessorContext, processorAttr.ContextType); // Populate UmbracoContext & ApplicationContext processorAttr.UmbracoContext = contextAccessor.UmbracoContext; processorAttr.ApplicationContext = contextAccessor.ApplicationContext; // Process value currentValue = processorAttr.ProcessValue(currentValue, ctx, chainContext); } } // The following has to happen after all the processors. if (isEnumerable && currentValue != null && currentValue.Equals(Enumerable.Empty <object>())) { if (propertyType.IsInterface) { // You cannot set an enumerable of type from an empty object array. currentValue = EnumerableInvocations.Cast(typeArg, (IEnumerable)currentValue); } else { // This should allow the casting back of IEnumerable<T> to an empty List<T> Collection<T> etc. // I cant think of any that don't have an empty constructor currentValue = propertyType.GetInstance(); } } return((currentValue == null && propertyType.IsValueType) ? propertyInfo.PropertyType.GetInstance() // Set to default instance of value type : currentValue); }
/// <summary> /// Returns an object representing the given <see cref="Type"/>. /// </summary> /// <param name="content"> /// The <see cref="IPublishedContent"/> to convert. /// </param> /// <param name="type"> /// The <see cref="Type"/> of items to return. /// </param> /// <param name="contextAccessor"> /// The context accessor. /// </param> /// <param name="culture"> /// The <see cref="CultureInfo"/> /// </param> /// <param name="instance"> /// An existing instance of T to populate /// </param> /// <param name="onConverting"> /// The <see cref="Action{ConversionHandlerContext}"/> to fire when converting. /// </param> /// <param name="onConverted"> /// The <see cref="Action{ConversionHandlerContext}"/> to fire when converted. /// </param> /// <param name="chainContext"> /// The <see cref="DittoChainContext"/> for the current processor chain. /// </param> /// <returns> /// The converted <see cref="Object"/> as the given type. /// </returns> /// <exception cref="InvalidOperationException"> /// Thrown if the given type has invalid constructors. /// </exception> private static object ConvertContent( IPublishedContent content, Type type, IDittoContextAccessor contextAccessor, CultureInfo culture, object instance, Action <DittoConversionHandlerContext> onConverting, Action <DittoConversionHandlerContext> onConverted, DittoChainContext chainContext) { // Collect all the properties of the given type and loop through writable ones. PropertyInfo[] properties; PropertyCache.TryGetValue(type, out properties); if (properties == null) { properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(x => x.CanWrite && x.GetSetMethod() != null).ToArray(); PropertyCache.TryAdd(type, properties); } // Check the validity of the mpped type constructor as early as possible. ParameterInfo[] constructorParams = type.GetConstructorParameters(); bool validConstructor = false; bool hasParameter = false; bool isType = false; bool hasLazy = false; if (constructorParams != null) { // Is it PublishedContentmModel or similar? if (constructorParams.Length == 1 && constructorParams[0].ParameterType == typeof(IPublishedContent)) { hasParameter = true; } if (constructorParams.Length == 0 || hasParameter) { validConstructor = true; } } // No valid constructor, but see if the value can be cast to the type if (type.IsInstanceOfType(content)) { isType = true; validConstructor = true; } if (!validConstructor) { throw new InvalidOperationException( $"Cannot convert IPublishedContent to {type} as it has no valid constructor. " + "A valid constructor is either an empty one, or one accepting a single IPublishedContent parameter."); } PropertyInfo[] lazyProperties = null; // If not already an instance, create an instance of the object if (instance == null) { // We can only proxy new instances. lazyProperties = properties.Where(x => x.ShouldAttemptLazyLoad()).ToArray(); if (lazyProperties.Any()) { hasLazy = true; var factory = new ProxyFactory(); instance = hasParameter ? factory.CreateProxy(type, lazyProperties.Select(x => x.Name), content) : factory.CreateProxy(type, lazyProperties.Select(x => x.Name)); } else if (isType) { instance = content; } else { // 1: This extension method is about 7x faster than the native implementation. // 2: Internally this uses Activator.CreateInstance which is heavily optimized. instance = hasParameter ? type.GetInstance(content) // 1 : type.GetInstance(); // 2 } } // We have the instance object but haven't yet populated properties // so fire the on converting event handlers OnConverting(content, type, culture, instance, onConverting); if (hasLazy) { // A dictionary to store lazily invoked values. var lazyMappings = new Dictionary <string, Lazy <object> >(); foreach (var propertyInfo in lazyProperties) { // Configure lazy properties using (DittoDisposableTimer.DebugDuration <Ditto>($"Lazy Property ({content.Id} {propertyInfo.Name})")) { // Ensure it's a virtual property (Only relevant to property level lazy loads) if (!propertyInfo.IsVirtualAndOverridable()) { throw new InvalidOperationException($"Lazy property '{propertyInfo.Name}' of type '{type.AssemblyQualifiedName}' must be declared virtual in order to be lazy loadable."); } lazyMappings.Add(propertyInfo.Name, new Lazy <object>(() => GetProcessedValue(content, culture, type, propertyInfo, instance, contextAccessor, chainContext))); } } ((IProxy)instance).Interceptor = new LazyInterceptor(lazyMappings); } // Process any non lazy properties foreach (var propertyInfo in properties.Where(x => !x.ShouldAttemptLazyLoad())) { // Check for the ignore attribute. if (propertyInfo.HasCustomAttribute <DittoIgnoreAttribute>()) { continue; } // Set the value normally. var value = GetProcessedValue(content, culture, type, propertyInfo, instance, contextAccessor, chainContext); // This over 4x faster as propertyInfo.SetValue(instance, value, null); FastPropertyAccessor.SetValue(propertyInfo, instance, value); } // We have now finished populating the instance object so go ahead // and fire the on converted event handlers OnConverted(content, type, culture, instance, onConverted); return(instance); }