using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
namespace Umbraco.Core.Models.PublishedContent
{
///
/// Implements a strongly typed content model factory
///
public class PublishedModelFactory : IPublishedModelFactory
{
private readonly Dictionary _modelInfos;
private class ModelInfo
{
public Type ParameterType { get; set; }
public Func Ctor { get; set; }
public Type ModelType { get; set; }
}
public Dictionary ModelTypeMap { get; }
///
/// 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)
{
var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
var exprs = new List>>();
ModelTypeMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
foreach (var type in types)
{
// fixme - annoying - we want the xpression to be of a give type
// fixme - but then do we need ctor(IPublishedElement x) and what about ctor(IPublishedContent x)?
//var expr = ReflectionUtilities.GetCtorExpr>(type, false);
//if (expr == null)
// throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type IPublishedElement.");
//var ccc = ReflectionUtilities.EmitCtor(type, false);
//if (ccc == null)
// throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type IPublishedElement.");
// 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 == 1 && typeof(IPublishedElement).IsAssignableFrom(parms[0].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.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\".");
//exprs.Add(Expression.Lambda>(Expression.New(constructor)));
modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, ModelType = type, Ctor = ReflectionUtilities.EmitCtor>(constructor) };
ModelTypeMap[typeName] = type;
}
//var compiled = ReflectionUtilities.CompileToDelegates(exprs.ToArray());
//var i = 0;
//foreach (var modelInfo in modelInfos.Values)
// modelInfo.Ctor = compiled[i++];
_modelInfos = modelInfos.Count > 0 ? modelInfos : null;
}
public IPublishedElement CreateModel(IPublishedElement element)
{
// fail fast
if (_modelInfos == null)
return element;
if (_modelInfos.TryGetValue(element.ContentType.Alias, out var modelInfo) == false)
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}.");
return modelInfo.Ctor(element);
}
}
}