using System.Collections; using System.Reflection; namespace Umbraco.Cms.Core.Models.PublishedContent { /// /// Implements a strongly typed content model factory /// public class PublishedModelFactory : IPublishedModelFactory { private readonly Dictionary? _modelInfos; private readonly Dictionary _modelTypeMap; private readonly IPublishedValueFallback _publishedValueFallback; private class ModelInfo { public Type? ParameterType { get; set; } public Func? Ctor { get; set; } public Type? ModelType { get; set; } public Func? ListCtor { get; set; } } /// /// Initializes a new instance of the class with types. /// /// The model types. /// /// Types must implement IPublishedContent and have a unique constructor that /// accepts one IPublishedContent as a parameter. /// To activate, /// /// var types = TypeLoader.Current.GetTypes{PublishedContentModel}(); /// var factory = new PublishedContentModelFactoryImpl(types); /// PublishedContentModelFactoryResolver.Current.SetFactory(factory); /// /// public PublishedModelFactory(IEnumerable types, IPublishedValueFallback publishedValueFallback) { var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var modelTypeMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var type in types) { // so... the model type has to implement a ctor with one parameter being, or inheriting from, // IPublishedElement - but it can be IPublishedContent - so we cannot get one precise ctor, // we have to iterate over all ctors and try to find the right one ConstructorInfo? constructor = null; Type? parameterType = null; foreach (var ctor in type.GetConstructors()) { var parms = ctor.GetParameters(); if (parms.Length == 2 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType) && typeof(IPublishedValueFallback).IsAssignableFrom(parms[1].ParameterType)) { if (constructor != null) { throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPublishedElement."); } constructor = ctor; parameterType = parms[0].ParameterType; } } if (constructor == null) { throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPublishedElement."); } var attribute = type.GetCustomAttribute(false); var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; if (modelInfos.TryGetValue(typeName, out var modelInfo)) { throw new InvalidOperationException($"Both types '{type.AssemblyQualifiedName}' and '{modelInfo.ModelType?.AssemblyQualifiedName}' want to be a model type for content type with alias \"{typeName}\"."); } // have to use an unsafe ctor because we don't know the types, really var modelCtor = ReflectionUtilities.EmitConstructorUnsafe>(constructor); modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, ModelType = type, Ctor = modelCtor }; modelTypeMap[typeName] = type; } _modelInfos = modelInfos.Count > 0 ? modelInfos : null; _modelTypeMap = modelTypeMap; _publishedValueFallback = publishedValueFallback; } /// public IPublishedElement CreateModel(IPublishedElement element) { // fail fast if (_modelInfos is null || element.ContentType.Alias is null || !_modelInfos.TryGetValue(element.ContentType.Alias, out var modelInfo)) { return element; } // ReSharper disable once UseMethodIsInstanceOfType if (modelInfo.ParameterType?.IsAssignableFrom(element.GetType()) == false) { throw new InvalidOperationException($"Model {modelInfo.ModelType} expects argument of type {modelInfo.ParameterType.FullName}, but got {element.GetType().FullName}."); } // can cast, because we checked when creating the ctor return (IPublishedElement)modelInfo.Ctor!(element, _publishedValueFallback); } /// public IList? CreateModelList(string? alias) { // fail fast if (_modelInfos is null || alias is null || !_modelInfos.TryGetValue(alias, out var modelInfo) || modelInfo.ModelType is null) { return new List(); } var ctor = modelInfo.ListCtor; if (ctor != null) { return ctor(); } var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); if (ctor is not null) { return ctor(); } return null; } /// public Type GetModelType(string? alias) { // fail fast if (_modelInfos is null || alias is null || !_modelInfos.TryGetValue(alias, out var modelInfo) || modelInfo.ModelType is null) { return typeof(IPublishedElement); } return modelInfo.ModelType; } /// public Type MapModelType(Type type) => ModelType.Map(type, _modelTypeMap); } }