Freedom Friday POC of native code first - limited to test project.

Making a few corrections to the serialization attributes after having tested ContentType Serialization.
Enabling bulk saving of new ContentTypes with Parent/Composition dependencies.
This commit is contained in:
Morten Christensen
2012-11-23 19:58:19 -01:00
parent c7693386a2
commit 1053492a29
36 changed files with 1155 additions and 22 deletions

View File

@@ -7,9 +7,18 @@ namespace Umbraco.Core.Models
/// Enum for the various statuses a Content object can have
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
[DataContract]
public enum ContentStatus
{
Unpublished, Published, Expired, Trashed, AwaitingRelease
[EnumMember]
Unpublished,
[EnumMember]
Published,
[EnumMember]
Expired,
[EnumMember]
Trashed,
[EnumMember]
AwaitingRelease
}
}

View File

@@ -27,7 +27,7 @@ namespace Umbraco.Core.Models
/// <summary>
/// Gets or sets the alias of the default Template.
/// </summary>
[DataMember]
[IgnoreDataMember]
public ITemplate DefaultTemplate
{
get { return AllowedTemplates.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId); }

View File

@@ -1,19 +1,21 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Models.Membership;
namespace Umbraco.Core.Models
{
/// <summary>
/// Represents an abstract class for base ContentType properties and methods
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
public abstract class ContentTypeBase : Entity, IContentTypeBase
{
private int _parentId;
private Lazy<int> _parentId;
private string _name;
private int _level;
private string _path;
@@ -31,7 +33,7 @@ namespace Umbraco.Core.Models
protected ContentTypeBase(int parentId)
{
_parentId = parentId;
_parentId = new Lazy<int>(() => parentId);
_allowedContentTypes = new List<ContentTypeSort>();
_propertyGroups = new PropertyGroupCollection();
}
@@ -64,14 +66,19 @@ namespace Umbraco.Core.Models
[DataMember]
public virtual int ParentId
{
get { return _parentId; }
get { return _parentId.Value; }
set
{
_parentId = value;
_parentId = new Lazy<int>(() => value);
OnPropertyChanged(ParentIdSelector);
}
}
public void SetLazyParentId(Lazy<int> id)
{
_parentId = id;
}
/// <summary>
/// Gets or sets the name of the current entity
/// </summary>

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
@@ -8,6 +9,8 @@ namespace Umbraco.Core.Models
/// <summary>
/// Represents an abstract class for composition specific ContentType properties and methods
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentTypeComposition
{
private List<IContentTypeComposition> _contentTypeComposition;

View File

@@ -1,4 +1,5 @@
using Umbraco.Core.Models.EntityBase;
using System;
using Umbraco.Core.Models.EntityBase;
namespace Umbraco.Core.Models
{
@@ -10,11 +11,16 @@ namespace Umbraco.Core.Models
/// <summary>
/// Gets or sets the Id of the ContentType
/// </summary>
public int Id { get; set; }
public Lazy<int> Id { get; set; }
/// <summary>
/// Gets or sets the Sort Order of the ContentType
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// Gets or sets the Alias of the ContentType
/// </summary>
public string Alias { get; set; }
}
}

View File

@@ -11,12 +11,16 @@ namespace Umbraco.Core.Models
/// but will be saved under the Ntext column.
/// </remarks>
[Serializable]
[DataContract(IsReference = true)]
[DataContract]
public enum DataTypeDatabaseType
{
[EnumMember]
Integer,
[EnumMember]
Date,
[EnumMember]
Nvarchar,
[EnumMember]
Ntext /*, Object*/
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Models.Membership;
@@ -86,6 +87,8 @@ namespace Umbraco.Core.Models
/// <summary>
/// Gets an enumerable list of Property Types aggregated for all groups
/// </summary>
IEnumerable<PropertyType> PropertyTypes { get; }
IEnumerable<PropertyType> PropertyTypes { get; }
void SetLazyParentId(Lazy<int> id);
}
}

View File

@@ -12,7 +12,7 @@ namespace Umbraco.Core.Models
/// Represents a collection of <see cref="PropertyGroup"/> objects
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
[DataContract]
public class PropertyGroupCollection : KeyedCollection<string, PropertyGroup>, INotifyCollectionChanged
{
private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim();

View File

@@ -25,7 +25,7 @@ namespace Umbraco.Core.Models
private int _sortOrder;
private string _validationRegExp;
public PropertyType(DataTypeDefinition dataTypeDefinition)
public PropertyType(IDataTypeDefinition dataTypeDefinition)
{
if(dataTypeDefinition.HasIdentity)
DataTypeId = dataTypeDefinition.Id;

View File

@@ -12,7 +12,7 @@ namespace Umbraco.Core.Models
/// Represents a collection of <see cref="PropertyType"/> objects
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
[DataContract]
public class PropertyTypeCollection : KeyedCollection<string, PropertyType>, INotifyCollectionChanged
{
private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim();

View File

@@ -31,6 +31,9 @@ namespace Umbraco.Core.Models
Key = name.EncodeAsGuid();
_name = name;
_alias = alias;
CreateDate = DateTime.UtcNow;
UpdateDate = DateTime.UtcNow;
}
[DataMember]

View File

@@ -90,7 +90,7 @@ namespace Umbraco.Core.Persistence.Repositories
//Insert collection of allowed content types
foreach (var allowedContentType in entity.AllowedContentTypes)
{
Database.Insert(new ContentTypeAllowedContentTypeDto { Id = entity.Id, AllowedId = allowedContentType.Id, SortOrder = allowedContentType.SortOrder });
Database.Insert(new ContentTypeAllowedContentTypeDto { Id = entity.Id, AllowedId = allowedContentType.Id.Value, SortOrder = allowedContentType.SortOrder });
}
var propertyFactory = new PropertyGroupFactory(nodeDto.NodeId);
@@ -145,7 +145,7 @@ namespace Umbraco.Core.Persistence.Repositories
//Insert collection of allowed content types
foreach (var allowedContentType in entity.AllowedContentTypes)
{
Database.Insert(new ContentTypeAllowedContentTypeDto { Id = entity.Id, AllowedId = allowedContentType.Id, SortOrder = allowedContentType.SortOrder });
Database.Insert(new ContentTypeAllowedContentTypeDto { Id = entity.Id, AllowedId = allowedContentType.Id.Value, SortOrder = allowedContentType.SortOrder });
}
//Check Dirty properties for Tabs/Groups and PropertyTypes - insert and delete accordingly
@@ -202,7 +202,7 @@ namespace Umbraco.Core.Persistence.Repositories
allowedContentTypesSql.Where("[cmsContentTypeAllowedContentType].[Id] = @Id", new { Id = id });
var allowedContentTypeDtos = Database.Fetch<ContentTypeAllowedContentTypeDto>(allowedContentTypesSql);
return allowedContentTypeDtos.Select(x => new ContentTypeSort { Id = x.AllowedId, SortOrder = x.SortOrder }).ToList();
return allowedContentTypeDtos.Select(x => new ContentTypeSort { Id = new Lazy<int>(() => x.AllowedId), SortOrder = x.SortOrder }).ToList();
}
protected PropertyGroupCollection GetPropertyGroupCollection(int id)

View File

@@ -532,7 +532,7 @@ namespace Umbraco.Core.Services
/// </summary>
/// <remarks>
/// This method ensures that Content is saved lazily, so a new graph of <see cref="IContent"/>
/// objects can be saved in bulk. But not that objects are saved one at a time to ensure Ids.
/// objects can be saved in bulk. But note that objects are saved one at a time to ensure Ids.
/// </remarks>
/// <param name="contents">Collection of Lazy <see cref="IContent"/> to save</param>
/// <param name="userId">Optional Id of the User saving the Content</param>

View File

@@ -109,6 +109,26 @@ namespace Umbraco.Core.Services
_unitOfWork.Commit();
}
/// <summary>
/// Saves a collection of lazy loaded <see cref="IContentType"/> objects.
/// </summary>
/// <remarks>
/// This method ensures that ContentType is saved lazily, so a new graph of <see cref="IContentType"/>
/// objects can be saved in bulk. But note that objects are saved one at a time to ensure Ids.
/// </remarks>
/// <param name="contentTypes">Collection of Lazy <see cref="IContentType"/> to save</param>
/// <param name="userId">Optional Id of the User saving the ContentTypes</param>
public void Save(IEnumerable<Lazy<IContentType>> contentTypes, int userId = -1)
{
var repository = RepositoryResolver.ResolveByType<IContentTypeRepository, IContentType, int>(_unitOfWork);
foreach (var content in contentTypes)
{
content.Value.CreatorId = 0;
repository.AddOrUpdate(content.Value);
_unitOfWork.Commit();
}
}
/// <summary>
/// Deletes a single <see cref="IContentType"/> object
/// </summary>

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Models;
@@ -48,6 +49,17 @@ namespace Umbraco.Core.Services
/// <param name="contentTypes">Collection of <see cref="IContentType"/> to save</param>
void Save(IEnumerable<IContentType> contentTypes);
/// <summary>
/// Saves a collection of lazy loaded <see cref="IContentType"/> objects.
/// </summary>
/// <remarks>
/// This method ensures that ContentType is saved lazily, so a new graph of <see cref="IContentType"/>
/// objects can be saved in bulk. But note that objects are saved one at a time to ensure Ids.
/// </remarks>
/// <param name="contentTypes">Collection of Lazy <see cref="IContentType"/> to save</param>
/// <param name="userId">Optional Id of the User saving the ContentTypes</param>
void Save(IEnumerable<Lazy<IContentType>> contentTypes, int userId = -1);
/// <summary>
/// Deletes a single <see cref="IContentType"/> object
/// </summary>

View File

@@ -322,6 +322,17 @@ namespace Umbraco.Core
return result;
}
/// <summary>
/// Splits a Pascal cased string into a phrase seperated by spaces.
/// </summary>
/// <param name="phrase">String to split</param>
/// <returns></returns>
public static string SplitPascalCasing(this string phrase)
{
string result = Regex.Replace(phrase, "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", "$1 ");
return result;
}
/// <summary>
/// Converts the phrase to specified convention.
/// </summary>

View File

@@ -0,0 +1,23 @@
using System;
namespace Umbraco.Tests.CodeFirst.Attributes
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class AliasAttribute : Attribute
{
public AliasAttribute(string @alias)
{
Alias = alias;
}
/// <summary>
/// Gets or Sets the Alias of the Property
/// </summary>
public string Alias { get; private set; }
/// <summary>
/// Gets or Sets an optional name of the Property
/// </summary>
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,52 @@
using System;
namespace Umbraco.Tests.CodeFirst.Attributes
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class ContentTypeAttribute : Attribute
{
public ContentTypeAttribute(string @alias)
{
Alias = alias;
IconUrl = "folder.gif";
Thumbnail = "folder.png";
Description = "";
}
/// <summary>
/// Gets or sets the Alias of the ContentType
/// </summary>
public string Alias { get; private set; }
/// <summary>
/// Gets or sets the optional Name of the ContentType
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the optional Description of the ContentType
/// </summary>
public string Description { get; set; }
/// <summary>
/// Gets or sets the optional IconUrl of the ContentType
/// </summary>
public string IconUrl { get; set; }
/// <summary>
/// Gets or sets the optional Thumbnail of the ContentType
/// </summary>
public string Thumbnail { get; set; }
/// <summary>
/// Gets or sets the optional array of Allowed Child ContentTypes of the ContentType
/// </summary>
public Type[] AllowedChildContentTypes { get; set; }
/// <summary>
/// Gets or sets the optional array of Allowed Template names of the ContentType
/// </summary>
public string[] AllowedTemplates { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace Umbraco.Tests.CodeFirst.Attributes
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class DescriptionAttribute : Attribute
{
public DescriptionAttribute(string description)
{
Description = description;
}
/// <summary>
/// Gets or sets the Description of the Property
/// </summary>
public string Description { get; private set; }
}
}

View File

@@ -0,0 +1,36 @@
using System;
namespace Umbraco.Tests.CodeFirst.Attributes
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class PropertyTypeAttribute : Attribute
{
public PropertyTypeAttribute(Type type)
{
Type = type;
PropertyGroup = "Generic Properties";
Mandatory = false;
ValidationRegExp = string.Empty;
}
/// <summary>
/// Gets or sets the Type of the DataType
/// </summary>
public Type Type { get; private set; }
/// <summary>
/// Gets or sets the Name of the PropertyGroup that this PropertyType belongs to
/// </summary>
public string PropertyGroup { get; set; }
/// <summary>
/// Boolean indicating that a value is required for this PropertyType
/// </summary>
public bool Mandatory { get; set; }
/// <summary>
/// Regular Expression for validating PropertyType's values
/// </summary>
public string ValidationRegExp { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System;
using umbraco.editorControls.tinyMCE3;
namespace Umbraco.Tests.CodeFirst.Attributes
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class RichtextAttribute : PropertyTypeAttribute
{
public RichtextAttribute()
: base(typeof(tinyMCE3dataType))
{
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace Umbraco.Tests.CodeFirst.Attributes
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class SortOrderAttribute : Attribute
{
public SortOrderAttribute(int order)
{
Order = order;
}
/// <summary>
/// Gets or sets the sort order of the Property
/// </summary>
public int Order { get; private set; }
}
}

View File

@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.ObjectResolution;
using Umbraco.Core.Serialization;
using Umbraco.Tests.CodeFirst.Definitions;
using Umbraco.Tests.CodeFirst.TestModels;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Entities;
using umbraco.editorControls.tinyMCE3;
using umbraco.interfaces;
namespace Umbraco.Tests.CodeFirst
{
[TestFixture]
public class CodeFirstTests : BaseDatabaseFactoryTest
{
[SetUp]
public override void Initialize()
{
UmbracoSettings.SettingsFilePath = IOHelper.MapPath(SystemDirectories.Config + Path.DirectorySeparatorChar, false);
//this ensures its reset
PluginManager.Current = new PluginManager();
//for testing, we'll specify which assemblies are scanned for the PluginTypeResolver
PluginManager.Current.AssembliesToScan = new[]
{
typeof(IDataType).Assembly,
typeof(tinyMCE3dataType).Assembly
};
DataTypesResolver.Current = new DataTypesResolver(
PluginManager.Current.ResolveDataTypes());
base.Initialize();
var serviceStackSerializer = new ServiceStackJsonSerializer();
SerializationService = new SerializationService(serviceStackSerializer);
}
[Test]
public void Can_Resolve_ContentType_From_Decorated_Home_Model()
{
var modelType = typeof(Home);
var contentType = ContentTypeDefinitionFactory.GetContentTypeDefinition(modelType);
Assert.That(contentType, Is.Not.Null);
Assert.That(contentType.Value.PropertyGroups, Is.Not.Null);
Assert.That(contentType.Value.PropertyTypes.Any(), Is.True);
Assert.That(contentType.Value.PropertyTypes.Count(), Is.EqualTo(2));
var result = SerializationService.ToStream(contentType.Value);
var xml = result.ResultStream.ToJsonString();
Console.WriteLine(xml);
}
[Test]
public void Can_Resolve_ContentType_From_Decorated_ContentPage_Model()
{
var modelType = typeof(ContentPage);
var contentType = ContentTypeDefinitionFactory.GetContentTypeDefinition(modelType);
Assert.That(contentType, Is.Not.Null);
Assert.That(contentType.Value.PropertyGroups, Is.Not.Null);
Assert.That(contentType.Value.PropertyGroups.Any(), Is.True);
Assert.That(contentType.Value.PropertyGroups.Count(), Is.EqualTo(1));
Assert.That(contentType.Value.PropertyTypes.Any(), Is.True);
Assert.That(contentType.Value.PropertyTypes.Count(), Is.EqualTo(1));
}
[Test]
public void Can_Resolve_ContentType_From_PlainPocoType_Model()
{
var modelType = typeof(PlainPocoType);
var contentType = ContentTypeDefinitionFactory.GetContentTypeDefinition(modelType);
Assert.That(contentType, Is.Not.Null);
Assert.That(contentType.Value.PropertyGroups, Is.Not.Null);
Assert.That(contentType.Value.PropertyTypes.Any(), Is.True);
Assert.That(contentType.Value.PropertyGroups.Count(), Is.EqualTo(1));
Assert.That(contentType.Value.PropertyTypes.Count(), Is.EqualTo(5));
var result = SerializationService.ToStream(contentType.Value);
var xml = result.ResultStream.ToJsonString();
Console.WriteLine(xml);
}
[Test]
public void Can_Retrieve_ContentTypes_After_Resolving()
{
ContentTypeDefinitionFactory.ClearContentTypeCache();
var modelType = typeof(Home);
var contentType = ContentTypeDefinitionFactory.GetContentTypeDefinition(modelType);
var mappedContentTypes = ContentTypeDefinitionFactory.RetrieveMappedContentTypes();
Assert.That(mappedContentTypes, Is.Not.Null);
Assert.That(mappedContentTypes.Any(), Is.True);
Assert.That(mappedContentTypes.Count(), Is.EqualTo(2));
}
[Test]
public void Can_Resolve_Existing_ContentType_With_Decorated_Model()
{
var textPage = MockedContentTypes.CreateTextpageContentType();
ServiceContext.ContentTypeService.Save(textPage);
var modelType = typeof(TextPage);
var contentType = ContentTypeDefinitionFactory.GetContentTypeDefinition(modelType);
Assert.That(contentType.Value.Id, Is.EqualTo(textPage.Id));
ServiceContext.ContentTypeService.Save(contentType.Value);
}
[Test]
public void Can_Save_Models_To_Database()
{
ContentTypeDefinitionFactory.ClearContentTypeCache();
var homeModel = typeof(Home);
var textPageModel = typeof(TextPage);
var homeContentType = ContentTypeDefinitionFactory.GetContentTypeDefinition(homeModel);
var textPageContentType = ContentTypeDefinitionFactory.GetContentTypeDefinition(textPageModel);
var mappedContentTypes = ContentTypeDefinitionFactory.RetrieveMappedContentTypes().ToList();
ServiceContext.ContentTypeService.Save(mappedContentTypes);
}
[Test]
public void Can_Resolve_Parent_Child_ContentTypes_And_Save_To_Database()
{
ContentTypeDefinitionFactory.ClearContentTypeCache();
var simplemodel = typeof(SimpleContentPage);
var model = typeof(AdvancedContentPage);
var sContentType = ContentTypeDefinitionFactory.GetContentTypeDefinition(simplemodel);
var aContentType = ContentTypeDefinitionFactory.GetContentTypeDefinition(model);
var mappedContentTypes = ContentTypeDefinitionFactory.RetrieveMappedContentTypes().ToList();
ServiceContext.ContentTypeService.Save(mappedContentTypes);
var type1 = ServiceContext.ContentTypeService.GetContentType(1045);
var type2 = ServiceContext.ContentTypeService.GetContentType(1046);
Assert.That(type1, Is.Not.Null);
Assert.That(type2, Is.Not.Null);
}
[Test]
public void Can_Resolve_And_Save_Decorated_Model_To_Database()
{
ContentTypeDefinitionFactory.ClearContentTypeCache();
var model = typeof(DecoratedModelPage);
var modelContentType = ContentTypeDefinitionFactory.GetContentTypeDefinition(model);
var mappedContentTypes = ContentTypeDefinitionFactory.RetrieveMappedContentTypes().ToList();
ServiceContext.ContentTypeService.Save(mappedContentTypes);
var type1 = ServiceContext.ContentTypeService.GetContentType(1047);
Assert.That(type1, Is.Not.Null);
Assert.That(type1.PropertyGroups.Count(), Is.EqualTo(2));
Assert.That(type1.PropertyTypes.Count(), Is.EqualTo(4));
}
private SerializationService SerializationService { get; set; }
private static int[] GetTopologicalSortOrder(IList<DependencyField> fields)
{
var g = new TopologicalSorter(fields.Count());
var _indexes = new Dictionary<string, int>();
//add vertices
for (int i = 0; i < fields.Count(); i++)
{
_indexes[fields[i].Alias.ToLower()] = g.AddVertex(i);
}
//add edges
for (int i = 0; i < fields.Count; i++)
{
if (fields[i].DependsOn != null)
{
for (int j = 0; j < fields[i].DependsOn.Length; j++)
{
g.AddEdge(i,
_indexes[fields[i].DependsOn[j].ToLower()]);
}
}
}
int[] result = g.Sort();
return result;
}
[TearDown]
public override void TearDown()
{
DatabaseContext.Database.Dispose();
//reset the app context
DataTypesResolver.Reset();
ApplicationContext.Current = null;
Resolution.IsFrozen = false;
string path = TestHelper.CurrentAssemblyDirectory;
AppDomain.CurrentDomain.SetData("DataDirectory", null);
ServiceContext = null;
SerializationService = null;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Umbraco.Tests.CodeFirst
{
public abstract class ContentTypeBase
{
}
}

View File

@@ -0,0 +1,383 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Tests.CodeFirst.Attributes;
using umbraco.interfaces;
namespace Umbraco.Tests.CodeFirst.Definitions
{
public static class ContentTypeDefinitionFactory
{
private static ConcurrentDictionary<string, DependencyField> _contentTypeCache = new ConcurrentDictionary<string, DependencyField>();
public static Lazy<IContentType> GetContentTypeDefinition(Type modelType)
{
//Check for BaseType different from ContentTypeBase
bool hasParent = modelType.BaseType != null && modelType.BaseType != typeof(ContentTypeBase) && modelType.BaseType != typeof(object);
var parent = new Lazy<IContentType>();
if(hasParent)
{
var isResolved = _contentTypeCache.ContainsKey(modelType.BaseType.FullName);
parent = isResolved
? _contentTypeCache[modelType.BaseType.FullName].ContentType
: GetContentTypeDefinition(modelType.BaseType);
}
var contentTypeAttribute = modelType.FirstAttribute<ContentTypeAttribute>();
var contentTypeAlias = contentTypeAttribute == null ? modelType.Name.ToUmbracoAlias() : contentTypeAttribute.Alias;
//Check if ContentType already exists by looking it up by Alias.
var existing = ServiceFactory.ContentTypeService.GetContentType(contentTypeAlias);
Lazy<IContentType> contentType = contentTypeAttribute == null
? PlainPocoConvention(modelType, existing)
: ContentTypeConvention(contentTypeAttribute, modelType, existing);
var definitions = new List<PropertyDefinition>();
int order = 0;
var objProperties = modelType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly).ToList();
foreach (var propertyInfo in objProperties)
{
var definition = new PropertyDefinition();
var aliasAttribute = propertyInfo.FirstAttribute<AliasAttribute>();
definition.Alias = PropertyTypeAliasConvention(aliasAttribute, propertyInfo.Name);
definition.Name = PropertyTypeNameConvention(aliasAttribute, propertyInfo.Name);
var descriptionAttribute = propertyInfo.FirstAttribute<DescriptionAttribute>();
definition.Description = descriptionAttribute != null ? descriptionAttribute.Description : string.Empty;
var sortOrderAttribute = propertyInfo.FirstAttribute<SortOrderAttribute>();
definition.Order = sortOrderAttribute != null ? sortOrderAttribute.Order : order;
var propertyTypeAttribute = propertyInfo.FirstAttribute<PropertyTypeAttribute>();
definition.Mandatory = propertyTypeAttribute != null && propertyTypeAttribute.Mandatory;
definition.ValidationRegExp = propertyTypeAttribute == null ? string.Empty : propertyTypeAttribute.ValidationRegExp;
definition.PropertyGroup = propertyTypeAttribute == null ? "Generic Properties" : propertyTypeAttribute.PropertyGroup;
definition.DataTypeDefinition = DataTypeConvention(propertyTypeAttribute, propertyInfo.PropertyType);
//RichtextAttribute convention
var richtextAttribute = propertyInfo.FirstAttribute<RichtextAttribute>();
if(richtextAttribute != null)
{
definition.DataTypeDefinition = DataTypeConvention(richtextAttribute, propertyInfo.PropertyType);
}
definitions.Add(definition);
order++;
}
//Loop through definitions for PropertyGroups and create those that not already exists
var groupDefinitions = definitions.DistinctBy(d => d.PropertyGroup);
foreach (var groupDefinition in groupDefinitions)
{
var groupExists = contentType.Value.PropertyGroups.Contains(groupDefinition.PropertyGroup);
if(groupExists == false)
{
var propertyGroup = new PropertyGroup {Name = groupDefinition.PropertyGroup};
contentType.Value.PropertyGroups.Add(propertyGroup);
}
}
//Loop through definitions for PropertyTypes and add them to the correct PropertyGroup
foreach (var definition in definitions)
{
var group = contentType.Value.PropertyGroups.First(x => x.Name == definition.PropertyGroup);
//Check if a PropertyType with the same alias already exists, as we don't want to override existing ones
if(group.PropertyTypes.Contains(definition.Alias)) continue;
var propertyType = new PropertyType(definition.DataTypeDefinition)
{
Mandatory = definition.Mandatory,
ValidationRegExp = definition.ValidationRegExp,
SortOrder = definition.Order,
Alias = definition.Alias,
Name = definition.Name
};
group.PropertyTypes.Add(propertyType);
}
//If current ContentType has a Parent the ParentId should be set and the ContentType added to the composition.
if(hasParent)
{
contentType.Value.SetLazyParentId(new Lazy<int>(() => parent.Value.Id));
contentType.Value.AddContentType(parent.Value);
}
//Add the resolved ContentType to the internal cache
var field = new DependencyField {ContentType = contentType, Alias = contentType.Value.Alias};
var dependencies = new List<string>();
if(hasParent)
dependencies.Add(parent.Value.Alias);
if(contentType.Value.AllowedContentTypes.Any())
{
dependencies.AddRange(contentType.Value.AllowedContentTypes.Select(allowed => allowed.Alias));
}
field.DependsOn = dependencies.ToArray();
_contentTypeCache.AddOrUpdate(modelType.FullName, field, (x, y) => field);
return contentType;
}
private static int[] GetTopologicalSortOrder(IList<DependencyField> fields)
{
var g = new TopologicalSorter(fields.Count());
var _indexes = new Dictionary<string, int>();
//add vertices
for (int i = 0; i < fields.Count(); i++)
{
_indexes[fields[i].Alias.ToLower()] = g.AddVertex(i);
}
//add edges
for (int i = 0; i < fields.Count; i++)
{
if (fields[i].DependsOn != null)
{
for (int j = 0; j < fields[i].DependsOn.Length; j++)
{
g.AddEdge(i,
_indexes[fields[i].DependsOn[j].ToLower()]);
}
}
}
int[] result = g.Sort();
return result;
}
public static IEnumerable<Lazy<IContentType>> RetrieveMappedContentTypes()
{
var fields = _contentTypeCache.Select(x => x.Value).ToList();
int[] sortOrder = GetTopologicalSortOrder(fields);
var list = new List<Lazy<IContentType>>();
for (int i = 0; i < sortOrder.Length; i++)
{
var field = fields[sortOrder[i]];
list.Add(field.ContentType);
Console.WriteLine(field.Alias);
if (field.DependsOn != null)
foreach (var item in field.DependsOn)
{
Console.WriteLine(" -{0}", item);
}
}
list.Reverse();
return list;
}
public static void ClearContentTypeCache()
{
_contentTypeCache.Clear();
}
/// <summary>
/// Convention to get a DataTypeDefinition from the PropertyTypeAttribute or the type of the property itself
/// </summary>
/// <param name="attribute"></param>
/// <param name="type"></param>
/// <returns></returns>
private static IDataTypeDefinition DataTypeConvention(PropertyTypeAttribute attribute, Type type)
{
if(attribute != null)
{
var instance = Activator.CreateInstance(attribute.Type);
var dataType = instance as IDataType;
return GetDataTypeByControlId(dataType.Id);
}
return TypeToPredefinedDataTypeConvention(type);
}
/// <summary>
/// Convention to get predefined DataTypeDefinitions based on the Type of the property
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private static IDataTypeDefinition TypeToPredefinedDataTypeConvention(Type type)
{
if(type == typeof(bool))
{
return GetDataTypeByControlId(new Guid("38b352c1-e9f8-4fd8-9324-9a2eab06d97a"));// Yes/No DataType
}
if(type == typeof(int))
{
return GetDataTypeByControlId(new Guid("1413afcb-d19a-4173-8e9a-68288d2a73b8"));// Number DataType
}
if(type == typeof(DateTime))
{
return GetDataTypeByControlId(new Guid("23e93522-3200-44e2-9f29-e61a6fcbb79a"));// Date Picker DataType
}
return GetDataTypeByControlId(new Guid("ec15c1e5-9d90-422a-aa52-4f7622c63bea"));// Standard textfield
}
/// <summary>
/// Gets the <see cref="IDataTypeDefinition"/> from the DataTypeService by its control Id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private static IDataTypeDefinition GetDataTypeByControlId(Guid id)
{
//TODO Create Definition if none exists
var definition = ServiceFactory.DataTypeService.GetDataTypeDefinitionByControlId(id);
return definition.FirstOrDefault();
}
/// <summary>
/// Convention to get the Alias of the PropertyType from the AliasAttribute or the property itself
/// </summary>
/// <param name="attribute"></param>
/// <param name="propertyName"></param>
/// <returns></returns>
private static string PropertyTypeAliasConvention(AliasAttribute attribute, string propertyName)
{
return attribute == null ? propertyName.ToUmbracoAlias() : attribute.Alias;
}
/// <summary>
/// Convention to get the Name of the PropertyType from the AliasAttribute or the property itself
/// </summary>
/// <param name="attribute"></param>
/// <param name="propertyName"></param>
/// <returns></returns>
private static string PropertyTypeNameConvention(AliasAttribute attribute, string propertyName)
{
if (attribute == null)
return propertyName.SplitPascalCasing();
return string.IsNullOrEmpty(attribute.Name) ? propertyName.SplitPascalCasing() : attribute.Name;
}
/// <summary>
/// Convention that converts a class decorated with the ContentTypeAttribute to an initial ContentType
/// </summary>
/// <param name="attribute"><see cref="ContentTypeAttribute"/> to use for mapping a <see cref="IContentType"/></param>
/// <param name="modelType">Type of the current class</param>
/// <param name="existing"> </param>
/// <returns>A Lazy <see cref="IContentType"/></returns>
private static Lazy<IContentType> ContentTypeConvention(ContentTypeAttribute attribute, Type modelType, IContentType existing)
{
var children = attribute.AllowedChildContentTypes == null
? new List<ContentTypeSort>()
: AllowedChildContentTypesConvention(
attribute.AllowedChildContentTypes, modelType);
var templates = attribute.AllowedTemplates == null
? new List<ITemplate>()
: AllowedTemplatesConvention(attribute.AllowedTemplates);
if(existing != null)
{
if (children.Any())
existing.AllowedContentTypes = children;
if (templates.Any())
existing.AllowedTemplates = templates;
return new Lazy<IContentType>(() => existing);
}
var contentType = new ContentType(-1)
{
Alias = attribute.Alias,
Description = attribute.Description,
Icon = attribute.IconUrl,
Thumbnail = attribute.Thumbnail,
Name = string.IsNullOrEmpty(attribute.Name)
? modelType.Name.SplitPascalCasing()
: attribute.Name,
AllowedContentTypes = children,
AllowedTemplates = templates
};
return new Lazy<IContentType>(() => contentType);
}
/// <summary>
/// Convention to resolve referenced templates
/// </summary>
/// <param name="templateNames"></param>
/// <returns></returns>
private static IEnumerable<ITemplate> AllowedTemplatesConvention(IEnumerable<string> templateNames)
{
var templates = new List<ITemplate>();
var engine = UmbracoSettings.DefaultRenderingEngine;
foreach (var templateName in templateNames)
{
var @alias = engine == RenderingEngine.Mvc
? templateName.Replace(".cshtml", "").Replace(".vbhtml", "")
: templateName.Replace(".masterpage", "");
var name = engine == RenderingEngine.Mvc
? string.Concat(@alias, ".cshtml")
: string.Concat(@alias, ".masterpage");
var template = ServiceFactory.FileService.GetTemplateByAlias(@alias);
if(template == null)
{
template = new Template(string.Empty, name, @alias) { CreatorId = 0, Content = string.Empty};
ServiceFactory.FileService.SaveTemplate(template);
}
templates.Add(template);
}
return templates;
}
/// <summary>
/// Convention to resolve referenced child content types
/// </summary>
/// <param name="types"></param>
/// <param name="currentType"> </param>
/// <returns></returns>
private static IEnumerable<ContentTypeSort> AllowedChildContentTypesConvention(IEnumerable<Type> types, Type currentType)
{
var contentTypeSorts = new List<ContentTypeSort>();
int order = 0;
foreach (var type in types)
{
if(type == currentType) continue;//If the referenced type is equal to the current type we skip it to avoid a circular dependency
var contentTypeSort = new ContentTypeSort();
var isResolved = _contentTypeCache.ContainsKey(type.FullName);
var lazy = isResolved ? _contentTypeCache[type.FullName].ContentType : GetContentTypeDefinition(type);
contentTypeSort.Id = new Lazy<int>(() => lazy.Value.Id);
contentTypeSort.Alias = lazy.Value.Alias;
contentTypeSort.SortOrder = order;
contentTypeSorts.Add(contentTypeSort);
order++;
}
return contentTypeSorts;
}
/// <summary>
/// Convention that converts a simple POCO to an initial ContentType
/// </summary>
/// <param name="modelType">Type of the object to map to a <see cref="IContentType"/></param>
/// <param name="existing"> </param>
/// <returns>A Lazy <see cref="IContentType"/></returns>
private static Lazy<IContentType> PlainPocoConvention(Type modelType, IContentType existing)
{
if(existing != null)
return new Lazy<IContentType>(() => existing);
var contentType = new ContentType(-1)
{
Alias = modelType.Name.ToUmbracoAlias(),
Description = string.Empty,
Icon = "folder.gif",
Thumbnail = "folder.png",
Name = modelType.Name.SplitPascalCasing(),
AllowedTemplates = new List<ITemplate>(),
AllowedContentTypes = new List<ContentTypeSort>()
};
return new Lazy<IContentType>(() => contentType);
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
using Umbraco.Core.Models;
namespace Umbraco.Tests.CodeFirst.Definitions
{
public class DependencyField
{
public string Alias { get; set; }
public string[] DependsOn { get; set; }
public Lazy<IContentType> ContentType { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
using System;
using Umbraco.Core.Models;
namespace Umbraco.Tests.CodeFirst.Definitions
{
public class PropertyDefinition
{
public string Alias { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int Order { get; set; }
public IDataTypeDefinition DataTypeDefinition { get; set; }
public string PropertyGroup { get; set; }
public bool Mandatory { get; set; }
public string ValidationRegExp { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System;
using Umbraco.Tests.CodeFirst.Attributes;
using umbraco.editorControls.textfield;
namespace Umbraco.Tests.CodeFirst.TestModels
{
public class AdvancedContentPage : SimpleContentPage
{
[PropertyType(typeof(TextFieldDataType), PropertyGroup = "Content")]
public string Author { get; set; }
public DateTime PublishDate { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
using Umbraco.Tests.CodeFirst.Attributes;
namespace Umbraco.Tests.CodeFirst.TestModels
{
public class ContentPage : ContentTypeBase
{
[Richtext(PropertyGroup = "Content")]
public string BodyContent { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
using System;
using Umbraco.Tests.CodeFirst.Attributes;
using umbraco.editorControls.textfield;
namespace Umbraco.Tests.CodeFirst.TestModels
{
[ContentType("modelPage", AllowedChildContentTypes = new[] { typeof(ContentPage) }, AllowedTemplates = new[]{"umbMaster"})]
public class DecoratedModelPage
{
[PropertyType(typeof(TextFieldDataType), PropertyGroup = "Content")]
public string Author { get; set; }
[PropertyType(typeof(TextFieldDataType), PropertyGroup = "Content")]
public string Title { get; set; }
[Richtext(PropertyGroup = "Content")]
public string BodyContent { get; set; }
public DateTime PublishDate { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
using Umbraco.Tests.CodeFirst.Attributes;
using umbraco.editorControls.textfield;
using umbraco.editorControls.textfieldmultiple;
namespace Umbraco.Tests.CodeFirst.TestModels
{
[ContentType("home", AllowedChildContentTypes = new[] { typeof(ContentPage) })]
public class Home
{
[PropertyType(typeof(TextFieldDataType))]
public string SiteName { get; set; }
[Alias("umbSiteDescription", Name = "Site Description")]
[PropertyType(typeof(textfieldMultipleDataType))]
public string SiteDescription { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace Umbraco.Tests.CodeFirst.TestModels
{
public class PlainPocoType
{
public string Title { get; set; }
public string Author { get; set; }
public bool IsFinished { get; set; }
public int Weight { get; set; }
public DateTime PublishDate { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using Umbraco.Tests.CodeFirst.Attributes;
using umbraco.editorControls.textfield;
namespace Umbraco.Tests.CodeFirst.TestModels
{
public class SimpleContentPage
{
[PropertyType(typeof(TextFieldDataType), PropertyGroup = "Content")]
public string Title { get; set; }
[Richtext(PropertyGroup = "Content")]
public string BodyContent { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using Umbraco.Tests.CodeFirst.Attributes;
using umbraco.editorControls.textfield;
namespace Umbraco.Tests.CodeFirst.TestModels
{
public class TextPage
{
[PropertyType(typeof(TextFieldDataType), PropertyGroup = "Content")]
public string Author { get; set; }
[PropertyType(typeof(TextFieldDataType), PropertyGroup = "Content")]
public string Title { get; set; }
}
}

View File

@@ -0,0 +1,118 @@
using System;
namespace Umbraco.Tests.CodeFirst
{
public class TopologicalSorter
{
#region - Private Members -
private readonly int[] _vertices; // list of vertices
private readonly int[,] _matrix; // adjacency matrix
private int _numVerts; // current number of vertices
private readonly int[] _sortedArray;
#endregion
#region - CTors -
public TopologicalSorter(int size)
{
_vertices = new int[size];
_matrix = new int[size, size];
_numVerts = 0;
for (int i = 0; i < size; i++)
for (int j = 0; j < size; j++)
_matrix[i, j] = 0;
_sortedArray = new int[size]; // sorted vert labels
}
#endregion
#region - Public Methods -
public int AddVertex(int vertex)
{
_vertices[_numVerts++] = vertex;
return _numVerts - 1;
}
public void AddEdge(int start, int end)
{
_matrix[start, end] = 1;
}
public int[] Sort() // toplogical sort
{
while (_numVerts > 0) // while vertices remain,
{
// get a vertex with no successors, or -1
int currentVertex = noSuccessors();
if (currentVertex == -1) // must be a cycle
throw new Exception("Graph has cycles");
// insert vertex label in sorted array (start at end)
_sortedArray[_numVerts - 1] = _vertices[currentVertex];
deleteVertex(currentVertex); // delete vertex
}
// vertices all gone; return sortedArray
return _sortedArray;
}
#endregion
#region - Private Helper Methods -
// returns vert with no successors (or -1 if no such verts)
private int noSuccessors()
{
for (int row = 0; row < _numVerts; row++)
{
bool isEdge = false; // edge from row to column in adjMat
for (int col = 0; col < _numVerts; col++)
{
if (_matrix[row, col] > 0) // if edge to another,
{
isEdge = true;
break; // this vertex has a successor try another
}
}
if (!isEdge) // if no edges, has no successors
return row;
}
return -1; // no
}
private void deleteVertex(int delVert)
{
// if not last vertex, delete from vertexList
if (delVert != _numVerts - 1)
{
for (int j = delVert; j < _numVerts - 1; j++)
_vertices[j] = _vertices[j + 1];
for (int row = delVert; row < _numVerts - 1; row++)
moveRowUp(row, _numVerts);
for (int col = delVert; col < _numVerts - 1; col++)
moveColLeft(col, _numVerts - 1);
}
_numVerts--; // one less vertex
}
private void moveRowUp(int row, int length)
{
for (int col = 0; col < length; col++)
_matrix[row, col] = _matrix[row + 1, col];
}
private void moveColLeft(int col, int length)
{
for (int row = 0; row < length; row++)
_matrix[row, col] = _matrix[row, col + 1];
}
#endregion
}
}

View File

@@ -89,6 +89,25 @@
<ItemGroup>
<Compile Include="Auditing\AuditTests.cs" />
<Compile Include="BusinessLogic\DictionaryTest.cs" />
<Compile Include="CodeFirst\Attributes\AliasAttribute.cs" />
<Compile Include="CodeFirst\Attributes\ContentTypeAttribute.cs" />
<Compile Include="CodeFirst\Attributes\DescriptionAttribute.cs" />
<Compile Include="CodeFirst\Attributes\PropertyTypeAttribute.cs" />
<Compile Include="CodeFirst\Attributes\RichtextAttribute.cs" />
<Compile Include="CodeFirst\Attributes\SortOrderAttribute.cs" />
<Compile Include="CodeFirst\CodeFirstTests.cs" />
<Compile Include="CodeFirst\ContentTypeBase.cs" />
<Compile Include="CodeFirst\Definitions\ContentTypeDefinitionFactory.cs" />
<Compile Include="CodeFirst\Definitions\DependencyField.cs" />
<Compile Include="CodeFirst\Definitions\PropertyDefinition.cs" />
<Compile Include="CodeFirst\TestModels\AdvancedContentPage.cs" />
<Compile Include="CodeFirst\TestModels\ContentPage.cs" />
<Compile Include="CodeFirst\TestModels\DecoratedModelPage.cs" />
<Compile Include="CodeFirst\TestModels\Home.cs" />
<Compile Include="CodeFirst\TestModels\PlainPocoType.cs" />
<Compile Include="CodeFirst\TestModels\SimpleContentPage.cs" />
<Compile Include="CodeFirst\TestModels\TextPage.cs" />
<Compile Include="CodeFirst\TopologicalSorter.cs" />
<Compile Include="Configurations\FileSystemProviderTests.cs" />
<Compile Include="Configurations\RepositorySettingsTests.cs" />
<Compile Include="ContentStores\PublishMediaStoreTests.cs" />